| name | web-client-usage |
| description | Conventions for writing JavaScript/TypeScript code that uses the Miden web SDK (`@miden-sdk/miden-sdk`). Use when building apps on Miden, writing integration tests, or calling MidenClient methods - covers initialization, the resource-based API (accounts, transactions, notes, keystore, compile), sync ordering, type conversions, transaction flows, custom contracts, private note transport, and pitfalls. |
NOTE: In the frontend-template context, prefer @miden-sdk/react hooks for all
operations they cover. Use the raw WebClient patterns documented here ONLY for
operations not available through React hooks (e.g., custom TransactionRequests,
direct store import/export, advanced sync control).
Web SDK Usage Patterns
This skill targets the @miden-sdk/miden-sdk npm package shipped from
0xMiden/web-sdk (e.g. crates/web-client/). For
React-hook usage, prefer the react-sdk-patterns skill - only fall through
to the raw client when a hook does not cover what you need.
API Overview
The SDK exposes a top-level MidenClient whose state is split across typed
resources:
| Resource | What it covers |
|---|
client.accounts | Wallets, faucets, custom contracts, listing, import/export |
client.transactions | send / mint / consume / consumeAll / swap / execute / preview / waitFor |
client.notes | Listing, fetching, importing/exporting, private-note transport |
client.tags | Note-tag subscriptions |
client.settings | Persistent client settings |
client.compile | Compiling MASM into account components, tx scripts, note scripts |
client.keystore | Inserting / fetching / removing secret keys |
MidenClient is the recommended public high-level surface. The lower-level WASM
clients are also exported for raw operations the resource API doesn't wrap: the
raw WASM WebClient (re-exported from the package's WASM crate via
export *) and WasmWebClient, a thin wrapper subclass of it (the .d.ts
declares WasmWebClient extends WasmWebClientBase, where WasmWebClientBase is
that raw WebClient). Both are importable from @miden-sdk/miden-sdk in a
bundler/browser build (the Node CJS entry happens not to re-export the bare
WebClient name, but tsc and Vite see it). Prefer MidenClient; reach for the
raw clients only for advanced operations not covered by a hook or the resource
API. To get a WasmWebClient, use the React useMidenClient() hook (it returns
one) or import it directly (the class is @internal but exported). MidenClient
keeps its wrapped client in a real JS private field (#inner), so external code
cannot reach in directly.
Client Initialization
Convenience constructors (recommended)
import { MidenClient } from "@miden-sdk/miden-sdk";
const client = await MidenClient.createTestnet();
const client = await MidenClient.createDevnet();
Both accept the same ClientOptions for overrides:
const client = await MidenClient.createTestnet({
storeName: "my-app-tests",
proverUrl: "local",
autoSync: false,
});
Generic constructor
const client = await MidenClient.create({
rpcUrl: "https://rpc.testnet.miden.io",
noteTransportUrl: "https://ntx.testnet.miden.io",
storeName: "my-store",
seed: new Uint8Array(32),
proverUrl: "testnet",
autoSync: true,
keystore: {
getKey: async (pubKey) => { },
insertKey: async (pubKey, secretKey) => { },
sign: async (pubKey, signingInputs) => { },
},
});
If rpcUrl is omitted, create() delegates to createTestnet().
Lazy / SSR-safe init
Some bundles (Next.js, Capacitor, raw /lazy entry) cannot await WASM at
import time. Use MidenClient.ready() to wait for WASM in-band - it is
idempotent and shared across callers:
await MidenClient.ready();
const client = await MidenClient.createTestnet();
Termination
client.terminate();
After terminate(), every method throws - guard against late callbacks on
unmount.
Sync - Always Sync First
The client's view of the chain is only as fresh as its last sync. Always
call sync() before reading account state or building a transaction that
depends on freshly received notes.
const summary = await client.sync();
const height = await client.getSyncHeight();
Common patterns:
- Sync before consuming notes (notes must be committed on-chain)
- Sync after submitting a transaction to observe the result
- Pass
waitForConfirmation: true to a transactions.send/mint/consume/swap
call to let the SDK wait for the tx commit instead of polling manually
- Use
client.waitForIdle() to flush all queued WASM calls before doing a
side-effect that must not race with a kernel callback (e.g. clearing an
in-memory unlock token after a wallet "lock")
autoSync: true (default for createTestnet/createDevnet) only triggers a
single sync at construction time - it is not a polling loop. Use the React
SDK's useSyncState or MidenProvider autoSyncInterval for periodic sync.
Type Conversions
Type confusion across the WASM boundary is the leading source of bugs.
AccountId
const id = AccountId.fromHex("0xabc123...");
const id = Address.fromBech32("mtst1abc...").accountId();
const hex = id.toString();
Pass AccountId (or any account ref the resource accepts: string hex,
Address, Account, AccountHeader) to resource methods - never raw
strings to methods that ask for AccountId directly.
AccountId.fromHex throws on malformed input; wrap in try/catch when
accepting user input.
Amounts - Always BigInt
BigInt(1000)
1000n
BigInt("1000")
All token amounts in the SDK are bigint. Mixing number causes silent
precision loss above Number.MAX_SAFE_INTEGER and TypeError below.
Visibility & Account Types
import { NoteVisibility, AccountType, AuthScheme, StorageMode } from "@miden-sdk/miden-sdk";
NoteVisibility.Public
NoteVisibility.Private
AccountType.FungibleFaucet
AccountType.NonFungibleFaucet
AuthScheme.Falcon
AuthScheme.ECDSA
StorageMode.Public
StorageMode.Private
Use NoteVisibility (not the legacy NoteType enum) and AuthScheme.Falcon
for the Poseidon2-based Falcon-512 scheme.
Account Creation
Type discriminator on auth: wallets and faucets take auth: AuthSchemeType (a "falcon" | "ecdsa" string-union, e.g. AuthScheme.Falcon); custom contracts take auth: AuthSecretKey (a concrete WASM instance).
const wallet = await client.accounts.create();
const wallet = await client.accounts.create({
storage: "private",
auth: AuthScheme.Falcon,
});
const faucet = await client.accounts.create({
type: AccountType.FungibleFaucet,
storage: "public",
symbol: "DAG",
decimals: 8,
maxSupply: 10_000_000n,
});
const component = await client.compile.component({ code: contractMasm, slots: [] });
const contract = await client.accounts.create({
seed: new Uint8Array(32),
auth: secretKey,
components: [component],
});
Transactions
The transactions API is option-bag-based and accepts any account ref
(Account, AccountHeader, hex string, AccountId).
Send
const { txId } = await client.transactions.send({
account: wallet,
to: "0xrecipient...",
token: faucet,
amount: 100n,
type: NoteVisibility.Public,
reclaimAfter: 100,
timelockUntil: 50,
waitForConfirmation: true,
timeout: 30_000,
});
For private sends where you also need to deliver the note out-of-band, set
returnNote: true and the call returns the constructed Note object -
incompatible with reclaimAfter/timelockUntil.
const { txId, note } = await client.transactions.send({
account: wallet,
to: recipientAddress,
token: faucet,
amount: 100n,
type: NoteVisibility.Private,
returnNote: true,
});
await client.notes.sendPrivate({ note, to: "mtst1..." });
Mint
const { txId } = await client.transactions.mint({
account: faucet,
to: targetAccountId,
amount: 1000n,
type: NoteVisibility.Public,
waitForConfirmation: true,
});
The transaction executes on the faucet - a frequent bug is passing the
recipient as account.
Consume
await client.transactions.consume({
account: wallet,
notes: [noteId1, noteRecord, "0xnote..."],
waitForConfirmation: true,
});
const { txId, consumed, remaining } = await client.transactions.consumeAll({
account: wallet,
maxNotes: 50,
});
Swap
await client.transactions.swap({
account: wallet,
offer: { token: tokenA, amount: 100n },
request: { token: tokenB, amount: 50n },
type: NoteVisibility.Public,
paybackType: NoteVisibility.Private,
});
Execute (custom scripts)
const script = await client.compile.txScript({
code: scriptMasm,
libraries: [{ namespace: "my::lib", code: libMasm, linking: "dynamic" }],
});
await client.transactions.execute({
account: contract,
script,
foreignAccounts: [
publicAccountId,
{ id: privateContractId, storage: storageRequirements },
],
waitForConfirmation: true,
});
Public foreign accounts are auto-fetched during execution - only private
foreign accounts must be supplied with their storage requirements.
Preview (dry run)
transactions.preview({ operation: "send" | "mint" | "consume" | "swap" | "custom", ... })
runs the same kernel as the real call but without proving or submitting,
returning a summary suitable for UI confirmation screens.
Notes
await client.notes.list();
await client.notes.list({ status: "committed" });
await client.notes.get(noteId);
await client.notes.listSent();
await client.notes.listAvailable({ account: wallet });
await client.notes.import(noteFile);
const file = await client.notes.export(noteId);
await client.notes.fetchPrivate();
await client.notes.sendPrivate({ note, to: addr });
Accounts (querying)
await client.accounts.list();
await client.accounts.get(ref);
await client.accounts.getOrImport(ref);
await client.accounts.getDetails(ref);
await client.accounts.insert({ account, overwrite });
For balance reads, use getDetails or - if you need a single asset balance
without loading the full vault - drop into the underlying WASM client's
accountReader(id) lazy reader.
Keystore
await client.keystore.insert(accountId, secretKey);
await client.keystore.get(pubKeyCommitment);
await client.keystore.remove(pubKeyCommitment);
await client.keystore.getCommitments(accountId);
await client.keystore.getAccountId(pubKeyCommitment);
keystore.insert is the single call that both stores the key and registers
its commitment with the account.
Compile
await client.compile.component({ code, slots, supportAllTypes: true });
await client.compile.txScript({ code, libraries });
await client.compile.noteScript({ code, libraries });
Note scripts are MASM libraries with a single @note_script-annotated
procedure, not begin/end programs - client.compile.noteScript builds the
correct shape from a procedure body. The same applies to @auth_script for
authentication scripts.
Common Workflows
Mint and consume (fund a fresh wallet)
const wallet = await client.accounts.create();
const faucet = await client.accounts.create({
type: AccountType.FungibleFaucet,
storage: "public",
symbol: "TEST",
decimals: 8,
maxSupply: 1_000_000n,
});
await client.transactions.mint({
account: faucet,
to: wallet,
amount: 10_000n,
type: NoteVisibility.Public,
waitForConfirmation: true,
});
await client.sync();
await client.transactions.consumeAll({
account: wallet,
waitForConfirmation: true,
});
Wait for an external transfer
await client.sync();
const before = (await client.notes.listAvailable({ account: wallet })).length;
while (true) {
await new Promise(r => setTimeout(r, 3000));
await client.sync();
const now = (await client.notes.listAvailable({ account: wallet })).length;
if (now > before) break;
}
Common Pitfalls
- Forgetting to sync. Notes won't appear, balances will be stale, foreign
accounts will be at the wrong block.
number instead of BigInt for amounts - silent precision loss.
- Passing strings where the SDK expects an account ref - pre-parse with
AccountId.fromHex() (and catch its throw).
- Consuming notes before they're committed - sync first, check status.
- Submitting
mint with the recipient as account - mint executes on
the faucet account, not the target.
- Private notes without transport - must call
notes.sendPrivate() (or
pass returnNote: true to transactions.send and deliver out-of-band).
- Holding WASM-owned objects across
terminate() - every Account,
Note, AccountId, NoteAndArgsArray etc. owns Rust memory through the
WASM ArrayBuffer. After terminate() they panic with "null pointer
passed to rust" - drop references on unmount.
- Calling
accountReader(...) in parallel with a write (the method lives
on the raw WasmWebClient, accessed via React's useMidenClient() or a
direct WasmWebClient import; not on MidenClient) - the readers share
the WASM client. Wrap concurrent flows with client.waitForIdle() or rely
on the React SDK's runExclusive. For multiple browser clients or tabs,
give each isolated client a distinct storeName; see the initialization
examples above and signer-integration for per-user store isolation.