| name | react-sdk-patterns |
| description | Complete guide to building Miden frontends with @miden-sdk/react hooks. Covers MidenProvider setup, all query hooks (useAccounts, useAccount, useNotes, useSyncState, useAssetMetadata), all mutation hooks (useCreateWallet, useSend, useMultiSend, useMint, useConsume, useSwap, useTransaction, useCreateFaucet), transaction stages, signer integration, and utility functions. Use when writing, editing, or reviewing Miden React frontend code. |
Miden React SDK Patterns
SDK Choice
ALWAYS use @miden-sdk/react hooks. Only fall back to the raw WasmWebClient (the low-level WASM client class returned by useMidenClient()) for operations not covered by hooks. The React SDK handles WASM safety (runExclusive), state management (Zustand), auto-sync, and transaction stage tracking automatically.
MidenProvider Configuration
import { MidenProvider } from "@miden-sdk/react";
<MidenProvider
config={{
rpcUrl: "testnet", // "devnet" | "testnet" | "localhost" | custom URL
prover: "testnet", // "local" | "devnet" | "testnet" | custom URL
autoSyncInterval: 15000, // ms, set to 0 to disable. Default: 15000
noteTransportUrl: "...", // optional: for private note delivery
}}
loadingComponent={<Loading />} // shown during WASM init
errorComponent={<Error />} // shown on init failure (receives Error prop)
>
<App />
</MidenProvider>
| Network | rpcUrl | Use When |
|---|
| Testnet | "testnet" | Recommended for new projects - primary development network |
| Devnet | "devnet" | Early-access testing (may lag feature parity with testnet) |
| Localhost | "localhost" | Local node at http://localhost:57291 |
Query Hooks
Each returns its own result shape plus isLoading, error, refetch.
useAccounts()
const { accounts, wallets, faucets, isLoading, error, refetch } = useAccounts();
useAccount(accountId: string)
const { account, assets, getBalance, isLoading, error, refetch } = useAccount(accountId);
useNotes(filter?)
const { notes, consumableNotes, noteSummaries, consumableNoteSummaries, isLoading, error, refetch } = useNotes();
const { notes } = useNotes({ accountId: "0x..." });
const { notes } = useNotes({ status: "committed" });
const { notes } = useNotes({ sender: "0x..." });
const { notes } = useNotes({ excludeIds: ["0xnote1", "0xnote2"] });
useNoteStream(options?)
const { notes, latest, markHandled, markAllHandled, snapshot, isLoading, error } = useNoteStream();
const { notes } = useNoteStream({ status: "committed", sender: "0x..." });
const { notes } = useNoteStream({ since: Date.now() - 60000 });
const { notes } = useNoteStream({ excludeIds: new Set(["0xnote1"]) });
const { notes } = useNoteStream({ amountFilter: (amount) => amount > 100n });
useSyncState()
const { syncHeight, isSyncing, lastSyncTime, sync, error } = useSyncState();
await sync();
useAssetMetadata(assetIds?: string[])
const { assetMetadata } = useAssetMetadata([faucetId]);
const meta = assetMetadata.get(faucetId);
useTransactionHistory(options?)
const { records, record, status, isLoading, error, refetch } = useTransactionHistory({ id: txId });
Mutation Hooks
Each returns its own action function plus isLoading, stage, error, reset.
Transaction stages: "idle" → "executing" → "proving" → "submitting" → "complete"
useCreateWallet()
const { createWallet, wallet, isCreating, error, reset } = useCreateWallet();
const account = await createWallet({
storageMode: "private",
authScheme: 2,
});
useCreateFaucet()
const { createFaucet, faucet, isCreating, error, reset } = useCreateFaucet();
const account = await createFaucet({
tokenSymbol: "TEST",
decimals: 8,
maxSupply: 1000000n,
storageMode: "public",
});
useImportAccount()
const { importAccount, account, isImporting, error, reset } = useImportAccount();
const account = await importAccount({ type: "id", accountId: "0x..." });
const account = await importAccount({ type: "file", file: accountFileOrBytes });
const account = await importAccount({ type: "seed", seed: seedBytes });
useSend()
const { send, result, isLoading, stage, error, reset } = useSend();
await send({
from: senderAccountId,
to: recipientAccountId,
assetId: faucetId,
amount: 1000n,
noteType: "private",
recallHeight: 100,
timelockHeight: 50,
sendAll: true,
attachment: [1n, 2n],
});
useMultiSend()
const { sendMany, result, isLoading, stage, error, reset } = useMultiSend();
await sendMany({
from: senderAccountId,
assetId: faucetId,
recipients: [
{ to: recipient1, amount: 500n },
{ to: recipient2, amount: 300n, noteType: "public" },
{ to: recipient3, amount: 200n, attachment: [1n, 2n, 3n] },
],
noteType: "private",
});
useMint()
const { mint, result, isLoading, stage, error, reset } = useMint();
await mint({
targetAccountId: recipientId,
faucetId: myFaucetId,
amount: 10000n,
noteType: "public",
});
useConsume()
const { consume, result, isLoading, stage, error, reset } = useConsume();
await consume({
accountId: myAccountId,
notes: [noteId1, noteId2],
});
useSwap()
const { swap, result, isLoading, stage, error, reset } = useSwap();
await swap({
accountId: myAccountId,
offeredFaucetId: tokenA,
offeredAmount: 100n,
requestedFaucetId: tokenB,
requestedAmount: 50n,
noteType: "private",
paybackNoteType: "private",
});
useTransaction() - Escape Hatch
const { execute, result, isLoading, stage, error, reset } = useTransaction();
await execute({ accountId, request: txRequest });
await execute({
accountId,
request: (client) => client.newSwapTransactionRequest(),
});
useWaitForCommit()
const { waitForCommit } = useWaitForCommit();
await waitForCommit(result.txId, {
timeoutMs: 10000,
intervalMs: 1000,
});
useWaitForNotes()
const { waitForConsumableNotes } = useWaitForNotes();
await waitForConsumableNotes({
accountId: myAccountId,
minCount: 1,
timeoutMs: 10000,
});
useSessionAccount(options)
const { initialize, sessionAccountId, isReady, step, error, reset } = useSessionAccount({
fund: async (sessionId) => {
await send({ from: mainWallet, to: sessionId, assetId: faucetId, amount: 100n });
},
assetId: faucetId,
walletOptions: {
storageMode: "private",
authScheme: 2,
},
pollIntervalMs: 3000,
});
Transaction Progress UI
function SendButton({ from, to, assetId, amount }) {
const { send, stage, isLoading, error } = useSend();
return (
<div>
<button onClick={() => send({ from, to, assetId, amount })} disabled={isLoading}>
{isLoading ? `${stage}...` : "Send"}
</button>
{error && <p>Error: {error.message}</p>}
</div>
);
}
Signer Integration
Local Keystore (Default)
No signer provider needed. Keys are managed in the browser via IndexedDB.
External Signers
Wrap MidenProvider with a signer provider. Three pre-built options:
ParaSignerProvider from @miden-sdk/para - EVM wallets
TurnkeySignerProvider from @miden-sdk/miden-turnkey-react - passkey auth
MidenFiSignerProvider from @miden-sdk/miden-wallet-adapter-react - MidenFi wallet
import { ParaSignerProvider } from "@miden-sdk/para";
<ParaSignerProvider apiKey="..." environment="PRODUCTION">
<MidenProvider config={...}><App /></MidenProvider>
</ParaSignerProvider>
useSigner() - Unified Interface
const { isConnected, connect, disconnect, name } = useSigner();
Custom Signer
Implement SignerContextValue interface via SignerContext.Provider. Requires: name, storeName (unique per user for DB isolation), accountConfig, signCb, connect, disconnect. See frontend-source-guide skill for source references.
Utility Functions
import { formatAssetAmount, parseAssetAmount, getNoteSummary, formatNoteSummary, toBech32AccountId } from "@miden-sdk/react";
formatAssetAmount(1000000n, 8)
parseAssetAmount("0.01", 8)
const summary = getNoteSummary(note);
formatNoteSummary(summary);
toBech32AccountId("0x1234...");
Direct Client Access
const client = useMidenClient();
const { runExclusive } = useMiden();
await runExclusive(async () => {
const header = await client.getBlockHeaderByNumber(100);
});
Type Imports
import type {
MidenConfig, QueryResult, MutationResult, TransactionStage,
AccountsResult, AccountResult, AssetBalance, NotesResult, NoteSummary,
SendOptions, MultiSendOptions, MintOptions, ConsumeOptions, SwapOptions,
CreateWalletOptions, CreateFaucetOptions, ExecuteTransactionOptions,
TransactionResult, SyncState, WaitForCommitOptions, WaitForNotesOptions,
Account, AccountId, InputNoteRecord, ConsumableNoteRecord,
TransactionRecord, TransactionRequest, NoteType, AccountStorageMode,
SignerContextValue, SignCallback, SignerAccountConfig,
} from "@miden-sdk/react";
Reading Account Storage
For the high-level vault summary on a tracked account, the existing query hook is enough:
const { account, assets, getBalance } = useAccount(accountId);
For lower-level reads (custom contract storage slots, map items), use Account.storage(). The React SDK serializes WASM access via runExclusive:
const client = useMidenClient();
const { runExclusive } = useMiden();
await runExclusive(async () => {
const id = AccountId.fromBech32(addressBech32);
if (!(await client.getAccount(id))) {
await client.importAccountById(id);
}
await client.syncState();
const account = await client.getAccount(id);
if (!account) return;
const value = account.storage().getItem("my_slot_name");
const mapValue = account.storage().getMapItem("my_map_slot", keyWord);
});
Account.storage() returns an AccountStorage. Both getItem(slot_name: string) and getMapItem(slot_name: string, key: Word) return Word | undefined. Use slot-name strings (e.g. COUNTER_SLOT_NAME in src/config.ts), not numeric indices. See src/hooks/useIncrementCounter.ts:73-83 for the live in-template getMapItem example.
useMidenClient() returns the raw WasmWebClient. Its direct methods include getAccount(accountId), getAccountStorage(accountId), importAccountById(accountId), syncState(), and the transaction-request factories (newSendTransactionRequest, newConsumeTransactionRequest, newMintTransactionRequest, newSwapTransactionRequest). For compile-from-source, call await client.createCodeBuilder() (returns Promise<CodeBuilder>) and use the resolved CodeBuilder's compileNoteScript(program: string) / compileTxScript(tx_script: string) (see CodeBuilder / createCodeBuilder() in miden_client_web.d.ts). The higher-level MidenClient.accounts.getOrImport resource API lives on the standalone MidenClient (see web-client-usage).
Account Import then Sync then Read Storage Flow
Hook-based pattern for the common "import a remote account, sync to current chain head, then read its state" workflow:
function ImportAndInspect({ accountIdHex }: { accountIdHex: string }) {
const { importAccount, isImporting } = useImportAccount();
const { sync, isSyncing, syncHeight } = useSyncState();
const { account, assets, getBalance, refetch } = useAccount(accountIdHex);
async function load() {
await importAccount({ type: "id", accountId: accountIdHex });
await sync();
await refetch();
}
}
useImportAccount accepts { type: "id" | "file" | "seed", ... }. After sync() resolves, the useAccount(accountIdHex) view reflects the latest chain state. For the raw WasmWebClient methods used by these hooks under the hood (client.getAccount, client.importAccountById, client.syncState), see src/hooks/useIncrementCounter.ts for a worked example. For the higher-level MidenClient.accounts.* resource API on a standalone MidenClient (outside React), see the web-client-usage skill.
Custom Notes and .masp Package Loading
.masp package files (compiled MASM artifacts) are produced by cargo miden build in the project-template/ workspace and copied into frontend-template/public/packages/ during the contract handoff (see CLAUDE.md "Contract Artifact Handoff" section).
The example below builds a custom transaction that emits two output notes carrying fungible assets. Each note has multi-felt input storage that the note script (compiled from MASM into .masp) reads and asserts on at consume time. The transaction is signed and submitted by the connected wallet via useMidenFiWallet().requestTransaction(...).
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
import {
Package,
NoteScript,
Note,
NoteAssets,
NoteMetadata,
NoteRecipient,
NoteStorage,
NoteTag,
NoteType,
NoteArray,
AccountId,
Felt,
FeltArray,
FungibleAsset,
TransactionRequestBuilder,
} from "@miden-sdk/miden-sdk";
import { randomWord } from "@/lib/miden";
const { requestTransaction } = useMidenFiWallet();
async function submitMultiNoteTx(
senderBech32: string,
targetBech32: string,
faucetBech32: string,
) {
const buf = await fetch("/packages/my_note.masp").then((r) => r.arrayBuffer());
const pkg = Package.deserialize(new Uint8Array(buf));
const noteScript = NoteScript.fromPackage(pkg);
const sender = AccountId.fromBech32(senderBech32);
const target = AccountId.fromBech32(targetBech32);
const faucet = AccountId.fromBech32(faucetBech32);
function makeRecipient(seedFelts: bigint[]): NoteRecipient {
const inputs = new FeltArray();
for (const v of seedFelts) inputs.push(new Felt(v));
return new NoteRecipient(randomWord(), noteScript, new NoteStorage(inputs));
}
const assets1 = new NoteAssets([new FungibleAsset(faucet, 1000n)]);
const assets2 = new NoteAssets([new FungibleAsset(faucet, 500n)]);
const tag = NoteTag.withAccountTarget(target);
const metadata = new NoteMetadata(sender, NoteType.Public, tag);
const note1 = new Note(assets1, metadata, makeRecipient([1n, 2n, 3n]));
const note2 = new Note(assets2, metadata, makeRecipient([4n, 5n, 6n]));
const txRequest = new TransactionRequestBuilder()
.withOwnOutputNotes(new NoteArray([note1, note2]))
.build();
const tx = Transaction.createCustomTransaction(senderBech32, targetBech32, txRequest);
if (!requestTransaction) throw new Error("Wallet does not support requestTransaction");
await requestTransaction(tx);
}
Notes on this pattern:
- Assertions live in MASM, not in TypeScript. The note script compiled into
.masp reads its NoteStorage felts at consume time and aborts the transaction if its assertions fail. The frontend's job is to construct and submit; MASM enforces. See project-template/contracts/ for where to author MASM with assertion ops.
- Single-output reference for simpler flows. For a simpler worked example using only one output note with empty assets and a single consume action, see
src/hooks/useIncrementCounter.ts. That hook is intentionally simpler and does not exercise asset transfers or multi-output emission.
- Compile from source (no
.masp) when needed. When the note script is not pre-bundled as .masp, compile it through CodeBuilder. compileNoteScript/compileTxScript are methods of CodeBuilder, not of WasmWebClient. The pattern is:
const client = useMidenClient();
const builder = await client.createCodeBuilder();
const noteScript = builder.compileNoteScript(noteSourceMasm);
const txScript = builder.compileTxScript(txSourceMasm);
See CodeBuilder and createCodeBuilder() in miden_client_web.d.ts. As the React-idiomatic alternative, useCompile() (@miden-sdk/react/dist/index.d.ts) wraps CompilerResource from the standalone MidenClient and exposes noteScript, txScript, component, isReady at the hook layer.
- Inside
useTransaction's request callback the parameter is a WasmWebClient (@miden-sdk/react/dist/index.d.ts). Use await client.createCodeBuilder() for compile, then build the TransactionRequest with TransactionRequestBuilder and return it. For the higher-level MidenClient.compile.* and MidenClient.transactions.execute resource API on a standalone MidenClient, see web-client-usage.
Cross-SDK Type Reference
The runtime types come from @miden-sdk/react (re-exported from @miden-sdk/miden-sdk for the underlying MidenClient types). The .d.ts files are the source of truth. Look them up in:
- the installed
.d.ts for @miden-sdk/react (hook return types and option types)
- the installed
.d.ts for @miden-sdk/miden-sdk (MidenClient, Account, AccountId, Note, Word, etc.)
Path layout differs across package managers (npm flat, pnpm nested, Yarn PnP virtual), so resolve them via your IDE's "Go to Definition" or the installed package surface rather than hard-coded paths.
Common app-developer types:
| Type | Source package | Notes |
|---|
Account, AccountHeader | @miden-sdk/react | id, nonce, bech32id() |
AccountId | @miden-sdk/miden-sdk | construct via AccountId.fromHex(hex); throws on invalid hex |
Address | @miden-sdk/miden-sdk | bech32 wrapper; Address.fromBech32(...) |
Note, InputNoteRecord, ConsumableNoteRecord | @miden-sdk/react | re-exported from @miden-sdk/miden-sdk. Input notes are received; for output-note types and private-note flows see web-client-usage. |
NoteVisibility (constants + string-union) | @miden-sdk/miden-sdk | const NoteVisibility = { Public: 'public', Private: 'private' } plus type NoteVisibility = 'public' | 'private' (api-types.d.ts). NOT an enum. Coexists with the raw WASM NoteType enum (miden_client_web.d.ts) which the template uses directly when building notes via the WASM types (see src/hooks/useIncrementCounter.ts). |
AccountType, AuthScheme, StorageMode | @miden-sdk/miden-sdk | enums; see web-client-usage "Visibility & Account Types". |
TransactionRequest | @miden-sdk/react | client factory functions return this |
Word | @miden-sdk/miden-sdk | 32-byte (4 felts) value; Word.toU64s() returns BigUint64Array of length 4 (each lane is a bigint after subscript). See Word.toU64s in miden_client_web.d.ts. |
Do not hardcode this table for long-term reference. The .d.ts files stay in lockstep with the installed package version; this list will drift.
Rust to TypeScript Type Mapping
The Rust (miden-client) and TypeScript (@miden-sdk/miden-sdk) SDKs share concepts but diverge on naming and primitive shapes. When porting between them:
| Concept | Rust (miden_objects / miden_client) | TypeScript (@miden-sdk/miden-sdk) |
|---|
| Token amount | u64 | bigint |
| Field element | Felt (Goldilocks u64 mod p) | Felt (wraps u64) |
| 32-byte word | Word ([Felt; 4]) | Word; toU64s(): BigUint64Array length 4 (each lane is bigint after subscript). |
| Account identifier | AccountId | AccountId; construct via AccountId.fromHex |
| Note visibility | NoteType enum | constants + string-union NoteVisibility ('public' | 'private') at the high-level MidenClient resource API; raw WASM NoteType enum (Private = 0, Public = 1) is also exported and used directly when constructing notes via the WASM types (see src/hooks/useIncrementCounter.ts). The two coexist; pick the layer your code lives in. |
| Account type | AccountType | AccountType enum |
| Authentication scheme | AuthScheme | AuthScheme enum |
| Storage mode | StorageMode | StorageMode enum |
Common gotchas:
- The TS method is
FeltArray.push(element: Felt) (miden_client_web.d.ts); there is no FeltArray.append. To convert a Felt to a JS bigint, use Felt.asInt() (miden_client_web.d.ts). When a Rust method appears missing in TS, consult node_modules/@miden-sdk/miden-sdk/dist/index.d.ts and dist/crates/miden_client_web.d.ts first instead of guessing the TS spelling.
- TS amounts are always
bigint. Mixing number causes silent precision loss above Number.MAX_SAFE_INTEGER and TypeError below.
Word.toU64s() returns BigUint64Array of length 4 (miden_client_web.d.ts). Each lane is a bigint (e.g. word.toU64s()[0]). Use it when reading the four u64 lanes from a Value storage slot or building assertions on Word outputs.
For the canonical Rust types, see 0xMiden/rust-sdk (the Rust client) and the miden_objects crate, which lives in 0xMiden/miden-base. For the canonical TS types, see node_modules/@miden-sdk/miden-sdk/dist/index.d.ts.