Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Enhancements

* [FEATURE][web,react] Added `WebClient.endpoint()` and `RpcClient.endpoint()`, returning the node URL the client is configured to talk to. The React SDK now derives the RPC endpoint from the live client instead of a separately-stored config copy: `useAssetMetadata` and `accountBech32` read `client.endpoint()`, so a configured (e.g. devnet) app no longer falls back to testnet during the provider's init window, and addresses are tagged for the right network (raw id when undetermined). ([#212](https://github.com/0xMiden/web-sdk/pull/212), companion: [0xMiden/miden-client#2291](https://github.com/0xMiden/miden-client/pull/2291))
* [FEATURE][web,react] The 0.14.x line's mobile/MT proving surface is available on the 0.15 series (forward-ported from `main`): `TransactionProver.newCallbackProver(jsFn)` (route prove to a native iOS/Android plugin; wire format matches `RemoteTransactionProver`), `ClientOptions.useWorker?: boolean` / `MidenConfig.useWorker` (opt out of the Web Worker shim — required for callback provers, whose closure cannot cross the worker boundary), `MidenClient._withInnerWebClient(fn)` with the depth-tracked re-entrancy fix, the multi-threaded WASM build at the `@miden-sdk/miden-sdk/mt` + `/mt/lazy` (and `@miden-sdk/react/mt` + `/mt/lazy`) subpaths, and the `miden-mobile-prover` C-ABI crate — now built against the 0.15 protocol, so its `TransactionInputs`/`ProvenTransaction` wire format requires a matching 0.15 SDK (native binaries built from 0.14 do not interoperate). ([#149](https://github.com/0xMiden/web-sdk/pull/149), [#152](https://github.com/0xMiden/web-sdk/pull/152), [#134](https://github.com/0xMiden/web-sdk/pull/134))
* [FEATURE][web] Added `BlockHeader.feeFaucetId()` — the account ID of the fungible faucet whose assets pay transaction verification fees, read from the block's on-chain fee parameters. This is the 0.15 spelling of the 0.14 line's `BlockHeader.nativeAssetId()` (the underlying protocol field was renamed `native_asset_id` → `fee_faucet_id`); consumers discovering the fee/native asset per network should migrate to the new name.

Expand Down
1 change: 1 addition & 0 deletions crates/web-client/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export {
const SYNC_METHODS = new Set([
"buildSwapTag",
"createCodeBuilder",
"endpoint",
"lastAuthError",
"newConsumeTransactionRequest",
"newMintTransactionRequest",
Expand Down
17 changes: 17 additions & 0 deletions crates/web-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ pub struct WebClient {
inner: AsyncCell<Option<Client<ClientAuth>>>,
mock_rpc_api: AsyncCell<Option<Arc<MockRpcApi>>>,
mock_note_transport_api: AsyncCell<Option<Arc<MockNoteTransportApi>>>,
// Cached at client creation from the RPC client's endpoint so page-side consumers can read it
// synchronously, without locking the async `inner` cell.
endpoint: std::sync::RwLock<Option<String>>,
}

// SAFETY: napi-rs with `tokio_rt` uses a multi-threaded tokio runtime, so async napi
Expand Down Expand Up @@ -268,6 +271,7 @@ impl WebClient {
inner: AsyncCell::new(None),
mock_rpc_api: AsyncCell::new(None),
mock_note_transport_api: AsyncCell::new(None),
endpoint: std::sync::RwLock::new(None),
}
}

Expand All @@ -279,6 +283,13 @@ impl WebClient {
Ok(client.store_identifier().to_string())
}

/// Returns the node endpoint URL this client is configured to talk to, or `undefined` if the
/// client has not been created yet (or its transport doesn't track an endpoint).
#[js_export(js_name = "endpoint")]
pub fn endpoint(&self) -> Option<String> {
self.endpoint.read().expect("endpoint lock poisoned").clone()
}

#[js_export(js_name = "createCodeBuilder")]
pub async fn create_code_builder(&self) -> Result<CodeBuilder, JsErr> {
let guard = self.inner.lock().await;
Expand Down Expand Up @@ -470,6 +481,9 @@ impl WebClient {
note_transport_client: Option<Arc<dyn NoteTransportClient>>,
debug_mode: Option<bool>,
) -> Result<(), JsValue> {
*self.endpoint.write().expect("endpoint lock poisoned") =
rpc_client.endpoint().map(str::to_string);

let mut builder = ClientBuilder::new()
.rpc(rpc_client)
.rng(Box::new(rng))
Expand Down Expand Up @@ -564,6 +578,9 @@ impl WebClient {
note_transport_client: Option<Arc<dyn NoteTransportClient>>,
debug_mode: Option<bool>,
) -> Result<(), JsErr> {
*self.endpoint.write().expect("endpoint lock poisoned") =
rpc_client.endpoint().map(str::to_string);

let client = maybe_wrap_send(async move {
let mut builder = ClientBuilder::new()
.rpc(rpc_client)
Expand Down
7 changes: 7 additions & 0 deletions crates/web-client/src/rpc_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ impl RpcClient {
Ok(RpcClient { inner: rpc_client })
}

/// Returns the endpoint URL this RPC client is configured to talk to, or `undefined` if its
/// transport doesn't track one.
#[js_export(js_name = "endpoint")]
pub fn endpoint(&self) -> Option<String> {
self.inner.endpoint().map(str::to_string)
}

/// Fetches notes by their IDs from the connected Miden node.
///
/// @param note_ids - Array of [`NoteId`] objects to fetch
Expand Down
18 changes: 18 additions & 0 deletions packages/react-sdk/src/__tests__/hooks/useAssetMetadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ beforeEach(() => {
useMidenStore.getState().reset();
mockGetAccountDetails.mockReset();
mockFromAccount.mockReset();
// useAssetMetadata derives the RPC endpoint from the live client, so it only fetches
// once a client exists. Provide one (the client carries its own endpoint).
useMidenStore.getState().setClient({
endpoint: () => "https://rpc.devnet.miden.io",
} as never);
});

describe("useAssetMetadata", () => {
Expand Down Expand Up @@ -151,6 +156,19 @@ describe("useAssetMetadata", () => {
expect(meta?.decimals).toBe(2);
});

it("defers fetching until a client exists (no RPC before the provider is ready)", async () => {
useMidenStore.getState().setClient(null);
mockGetAccountDetails.mockResolvedValue({ account: () => ({ id: "x" }) });

const { result } = renderHook(() => useAssetMetadata(["0xfaucet7"]));

// Give the effect a chance to run; with no client it must not fetch.
await new Promise((r) => setTimeout(r, 50));

expect(mockGetAccountDetails).not.toHaveBeenCalled();
expect(result.current.assetMetadata.has("0xfaucet7")).toBe(false);
});

it("should filter out falsy asset IDs", () => {
const { result } = renderHook(() =>
useAssetMetadata(["", undefined as unknown as string, ""])
Expand Down
2 changes: 2 additions & 0 deletions packages/react-sdk/src/__tests__/mocks/miden-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const createMockWebClient = (
const defaultClient: MockWebClientType = {
// Initialization
createClient: vi.fn().mockResolvedValue(undefined),
endpoint: vi.fn(() => "https://rpc.devnet.miden.io"),

// Account methods
getAccounts: vi.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -299,6 +300,7 @@ export const createMockWebClient = (

type MockWebClientType = {
createClient: ReturnType<typeof vi.fn>;
endpoint: ReturnType<typeof vi.fn>;
getAccounts: ReturnType<typeof vi.fn>;
getAccount: ReturnType<typeof vi.fn>;
newWallet: ReturnType<typeof vi.fn>;
Expand Down
1 change: 1 addition & 0 deletions packages/react-sdk/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ vi.mock("@miden-sdk/miden-sdk", () => {
| null,
setSignCb: vi.fn(),
free: vi.fn(),
endpoint: vi.fn(() => "https://rpc.devnet.miden.io"),
};

const WebClient = Object.assign(
Expand Down
11 changes: 9 additions & 2 deletions packages/react-sdk/src/hooks/useAssetMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,15 @@ const fetchAssetMetadata = async (
export function useAssetMetadata(assetIds: string[] = []) {
const assetMetadata = useAssetMetadataStore();
const setAssetMetadata = useMidenStore((state) => state.setAssetMetadata);
const rpcUrl = useMidenStore((state) => state.config.rpcUrl);
const rpcClient = useMemo(() => getRpcClient(rpcUrl), [rpcUrl]);
// Derive the endpoint from the live client (single source of truth) rather than a
// separately-stored config copy. `client` exists only once MidenProvider has finished
// initializing, so a configured app never reads an unset URL and falls back to testnet
// during the init window.
const client = useMidenStore((state) => state.client);
const rpcClient = useMemo(
() => (client ? getRpcClient(client.endpoint() ?? undefined) : null),
[client]
);

const uniqueAssetIds = useMemo(
() => Array.from(new Set(assetIds.filter(Boolean))),
Expand Down
36 changes: 23 additions & 13 deletions packages/react-sdk/src/utils/accountBech32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ type AccountPrototype = {
bech32id?: () => string;
};

const inferNetworkId = (): NetworkId => {
const { rpcUrl } = useMidenStore.getState().config;
if (!rpcUrl) {
return NetworkId.testnet();
// Derive the bech32 network from the live client's endpoint (single source of truth).
// A real client always carries an endpoint (testnet by default), so the only
// undetermined cases are: no client yet (provider still initializing) or a configured
// custom endpoint we can't map — both return `null` so callers fall back to the raw
// account id rather than tagging it for the wrong network.
const resolveNetworkId = (): NetworkId | null => {
const url = useMidenStore.getState().client?.endpoint()?.toLowerCase();
if (!url) {
return null;
}

const url = rpcUrl.toLowerCase();
if (url.includes("devnet") || url.includes("mdev")) {
return NetworkId.devnet();
}
Expand All @@ -28,23 +31,30 @@ const inferNetworkId = (): NetworkId => {
if (url.includes("testnet") || url.includes("mtst")) {
return NetworkId.testnet();
}

return NetworkId.testnet();
if (url.includes("localhost") || url.includes("127.0.0.1")) {
// Local nodes run a devnet genesis by default.
return NetworkId.devnet();
}
return null;
};

const toBech32FromAccountId = (id: AccountId): string => {
const networkId = resolveNetworkId();
// Network not yet determinable (provider initializing, or a custom endpoint):
// return the raw id rather than risk a wrong-network bech32 address.
if (!networkId) {
return id.toString();
}

try {
const address = Address.fromAccountId(id, "BasicWallet");
return address.toBech32(inferNetworkId());
return address.toBech32(networkId);
} catch {
// Fall through to AccountId conversion or string fallback.
}

try {
const maybeBech32 = id.toBech32?.(
inferNetworkId(),
AccountInterface.BasicWallet
);
const maybeBech32 = id.toBech32?.(networkId, AccountInterface.BasicWallet);
if (typeof maybeBech32 === "string") {
return maybeBech32;
}
Expand Down
Loading