diff --git a/README.md b/README.md
index 03fcc2c..351ad8f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,15 @@
Skills and commands for AI agents when working with the Miden ecosystem.
+**Scope.** This repository is canonical for knowledge that spans the ecosystem:
+Miden Assembly, the Rust SDK, protocol concepts, and the general-purpose slash
+commands. Anything describing the `@miden-sdk/*` JavaScript API now lives in
+[`0xMiden/web-sdk`](https://github.com/0xMiden/web-sdk) and ships inside the npm
+packages, so it stays matched to the version a project has installed — see
+[Web SDK and React](#web-sdk-and-react--now-shipped-with-the-sdk-itself) below.
+A good rule of thumb: if a skill would mention `@miden-sdk/…`, it belongs there;
+otherwise it belongs here.
+
## Skills
Skills are applied automatically when the agent detects relevant tasks. Each skill is a `SKILL.md` file in a named directory under `skills/`.
@@ -15,14 +24,33 @@ Skills are applied automatically when the agent detects relevant tasks. Each ski
- **rust-sdk-pitfalls** – Critical safety rules: felt arithmetic, comparison operators, stack limits, argument limits, storage naming, no-std
- **rust-sdk-source-guide** – Advanced development guide: AI practices (Plan Mode, verification-driven development, sub-agents, context engineering) and Miden source repository map for discovering patterns beyond basic skills
-### React Frontend Development
+### Web SDK and React — now shipped with the SDK itself
+
+Frontend skills are no longer kept here. They live in
+[`0xMiden/web-sdk`](https://github.com/0xMiden/web-sdk) and ship inside the npm
+packages they document, so they are always version-matched to the SDK a project
+actually has installed:
+
+| Skill | Read it at |
+|---|---|
+| `web-client-usage`, `frontend-pitfalls`, `signer-integration`, `frontend-source-guide` | `node_modules/@miden-sdk/miden-sdk/skills/` |
+| `react-sdk-patterns`, `testing-patterns` | `node_modules/@miden-sdk/react/skills/` |
+| `vite-wasm-setup` | `node_modules/@miden-sdk/vite-plugin/skills/` |
+
+To pull them into a project, run `npm create @miden-sdk@latest`. It writes a
+pointer into the project's `AGENTS.md`, copies the skills into
+`.claude/skills/`, and adds a `prepare` script so they refresh on every install.
+
+**Why they moved.** These skills describe the `@miden-sdk/*` API. Kept in a
+separate repository, nothing tied a skill to the API it documented, so an API
+change and its documentation were always two PRs — and the copies here and in
+`0xMiden/frontend-template` had drifted from each other and from the code.
+Beside the source, they change in the same PR as the API. See
+[web-sdk#310](https://github.com/0xMiden/web-sdk/pull/310).
-- **react-sdk-patterns** – Complete `@miden-sdk/react` hook API reference: MidenProvider, query hooks, mutation hooks, transaction stages, signer integration, utilities
-- **frontend-pitfalls** – Critical frontend pitfalls: WASM init race, recursive access crash, COOP/COEP headers, BigInt handling, Bech32 mismatch, IndexedDB state loss
-- **vite-wasm-setup** – Vite + WASM configuration: required plugins, deployment headers (Nginx, Vercel, Cloudflare), TypeScript config, troubleshooting
-- **frontend-source-guide** – Advanced frontend development guide: AI practices and miden-client source repository map for discovering patterns beyond basic skills
-- **signer-integration** – Integrating external signers (Para, Turnkey, MidenFi wallet adapter) and building custom signers for Miden React frontends
-- **testing-patterns** – Testing conventions: Vitest + testing-library setup, `@miden-sdk/react` module mocking, fixtures, TDD workflow for Miden React components
+`idxdb-patterns` and `wasm-bridge` moved for the same reason and now live in
+web-sdk's own `.claude/skills/`. They document internals rather than the public
+API, so they are deliberately not published to npm.
### Miden Assembly
@@ -31,12 +59,9 @@ Skills are applied automatically when the agent detects relevant tasks. Each ski
- **masm-padding** – Stack padding conventions for `call` vs `exec` procedures
- **masm-formatting** – Orchestrator covering capitalization, `(N)` span notation, cross-repo doc-comment divergences, `Cycles:`, and chained assertion style
-### Miden Client (Web SDK & Internals)
+### Miden Client (Rust)
- **rust-client-patterns** – Rust conventions for the `miden-client` crate: error handling (`thiserror` + `ErrorHint`), `Store` trait (adding methods across `SqliteStore`/`WebStore` with cross-platform `async_trait`), `Client` generic pattern with the `Keystore` super-trait, `no_std` imports (`alloc::`/`core::`), `ClientBuilder` network constructors (`for_testnet()`, `for_devnet()`, `for_localhost()`), and section header formatting (`// ===` top-level, `// ---` subsections)
-- **wasm-bridge** – Rust↔JS WASM boundary conventions for the `web-client` crate: `#[wasm_bindgen]` method exposure with `js_name`, newtype wrappers with `From` conversions, `js_error_with_context` error chaining with `ErrorHint`, promise handling (`await_js`/`await_ok`/`await_js_value`), data transfer objects with `getter_with_clone`, JS function imports, and the `MidenClient`/`WasmWebClient` (`WebClient`) two-layer JS API
-- **idxdb-patterns** – IndexedDB/Dexie persistence conventions for the `idxdb-store` crate: Dexie transactions (`db.dexie.transaction("rw", tables, ...)`), schema interfaces (`IAccount`, `IAccountCode`), database registry (`getDatabase`/`openDatabase`), `logWebStoreError` error handling, forward-only state updates, and the TS→JS dual-commit build workflow
-- **web-client-usage** – Developer-facing patterns for using the `@miden-sdk/miden-sdk` npm package: `MidenClient.create()` initialization, the resource-based API (`client.accounts`, `client.transactions`, `client.notes`, `client.tags`, `client.settings`, `client.compile`, `client.keystore`), sync ordering, type conversions (`AccountId.fromHex`, `BigInt` amounts, `NoteVisibility`), transaction flows (mint, send, consume, swap, custom contracts), private note transport, querying, import/export, and pitfall avoidance
## Commands
diff --git a/skills/frontend-pitfalls/SKILL.md b/skills/frontend-pitfalls/SKILL.md
deleted file mode 100644
index f99ede9..0000000
--- a/skills/frontend-pitfalls/SKILL.md
+++ /dev/null
@@ -1,202 +0,0 @@
----
-name: frontend-pitfalls
-description: Critical pitfalls and safety rules for Miden frontend development. Covers WASM initialization, concurrent access crashes, COOP/COEP headers, BigInt handling, Bech32 network mismatches, IndexedDB state loss, auto-sync side effects, Vite configuration, and React rendering race conditions. Use when reviewing, debugging, or writing Miden frontend code.
----
-
-# Miden Frontend Pitfalls
-
-## FP1: WASM Initialization Race (CRITICAL)
-
-Components that use Miden hooks before MidenProvider finishes WASM initialization will crash.
-
-```tsx
-// WRONG — renders empty before WASM is ready
-function App() {
- const { accounts } = useAccounts(); // returns empty arrays before WASM is ready
- return
{accounts.length}
;
-}
-
-// CORRECT — use loadingComponent or check isReady
-Loading WASM...
;
- return ;
-}
-```
-
-## FP2: Recursive WASM Access Crash (CRITICAL)
-
-The WASM client is single-threaded. Concurrent calls crash with "recursive use of an object detected".
-
-```tsx
-// WRONG — two operations running simultaneously
-const handleClick = async () => {
- sync(); // fires async
- await send({ ... }); // runs concurrently — CRASH
-};
-
-// CORRECT — use runExclusive for sequential execution
-const client = useMidenClient();
-const { runExclusive } = useMiden();
-await runExclusive(async () => {
- await client.syncState();
- // now safe to do next operation
-});
-```
-
-Built-in hooks (useSend, useConsume, etc.) already use runExclusive internally. This pitfall applies when using `useMidenClient()` directly or mixing manual client calls with hook mutations.
-
-## FP3: COOP/COEP Headers — Only for the Multi-Threaded (MT) Build (HIGH)
-
-COOP/COEP cross-origin-isolation is **not** a universal requirement. The web SDK ships four entry points along two axes (eager/lazy × ST/MT), and the isolation requirement depends entirely on the threading model:
-
-- The **default** `@miden-sdk/react` (and `@miden-sdk/react/lazy`) and the **default** `@miden-sdk/miden-sdk` (and `/lazy`) are **single-threaded (ST)**. They ship single-threaded WASM that "loads in any browser context" with **no COOP/COEP requirement**. This is why the SDK's shipped example wallet runs full Miden client code (`MidenProvider`) importing the default `@miden-sdk/react` while using the bare `midenVitePlugin()` with no cross-origin isolation — ST simply does not need it.
-- Only the **multi-threaded (MT)** variants — `@miden-sdk/react/mt`, `@miden-sdk/react/mt/lazy`, `@miden-sdk/miden-sdk/mt`, `@miden-sdk/miden-sdk/mt/lazy` (wasm-bindgen-rayon, ~3–5× faster local proving) — **require** the page to be cross-origin-isolated (`self.crossOriginIsolated === true`). Without `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp`, the browser refuses to construct `WebAssembly.Memory({ shared: true })` and the MT WASM fails to instantiate at module load.
-
-So: pick ST (the default) and you need no headers at all; opt into MT only if you do local proving on a host whose headers you control.
-
-If you DO opt into the MT build, enable isolation via the Vite plugin explicitly on any route that runs the MT client:
-
-```ts
-// in your app's vite.config.ts — only needed for the MT build
-import { midenVitePlugin } from "@miden-sdk/vite-plugin";
-
-export default defineConfig({
- plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
-});
-```
-
-Do not rely on the plugin's own default — `@miden-sdk/vite-plugin` defaults `crossOriginIsolation` to `false` (verified false in the executable source across the released tags; note the plugin README incorrectly says the default is `true`). For MT you must pass `true` explicitly. For ST (the default build) leaving it `false` is correct — the example wallet uses bare `midenVitePlugin()` precisely because it is ST, and because `same-origin` COOP would nullify `window.opener` in the Para OAuth popups it pairs with via `paraVitePlugin()`.
-
-For MT, COOP/COEP must also be set on the production server — the plugin covers only the Vite dev and preview servers, not your real production host. See `vite-wasm-setup` for per-host configs (Nginx, Vercel, Cloudflare).
-
-**Gotcha (when isolation is on)**: Cross-origin-isolation breaks third-party iframes, external scripts without CORS, and OAuth popups. If a route must host those and cannot satisfy isolation, stay on the default ST subpaths (they need no isolation) or, if you genuinely need MT elsewhere, use `Cross-Origin-Embedder-Policy: credentialless` for weaker isolation that still allows most cross-origin resources, or scope the headers to only the MT routes. Do not enable isolation globally as a convenience.
-
-## FP4: BigInt at the Raw WASM Boundary (HIGH)
-
-The React SDK hooks (`useSend`, `useCreateFaucet`, `useMultiSend`, …) accept `bigint | number` for amounts and coerce to `bigint` internally — `SendOptions.amount` and `CreateFaucetOptions.maxSupply` are both typed `bigint | number`, and `useCreateFaucet` calls `BigInt(options.maxSupply)` before forwarding. So `number` does NOT fail at the hook layer. `bigint` is required only at the raw WASM client (`@miden-sdk/miden-sdk`) boundary, where amounts are `bigint` with no coercion.
-
-```tsx
-// FINE at the React-SDK hook layer — number is coerced
-await send({ from, to, assetId, amount: 1000 });
-await createFaucet({ maxSupply: 1000000, ... });
-
-// ALSO FINE — pass bigint directly (preferred; avoids precision loss above 2^53)
-await send({ from, to, assetId, amount: 1000n });
-await createFaucet({ maxSupply: BigInt(1000000), ... });
-
-// REQUIRED at the raw WASM client boundary — must be bigint
-// (the low-level @miden-sdk/miden-sdk client does not coerce number)
-
-// CORRECT — use parseAssetAmount for user input (decimal string → bigint)
-import { parseAssetAmount } from "@miden-sdk/react";
-const amount = parseAssetAmount(inputValue, 8); // string → bigint
-```
-
-Prefer `bigint` everywhere anyway: a `number` above `2^53` loses precision before it ever reaches the coercion, so large supplies/amounts must be `bigint` or a decimal string parsed via `parseAssetAmount`.
-
-**Gotcha**: `JSON.stringify` cannot serialize `bigint`. Use a custom replacer or convert to string first.
-
-## FP5: Bech32 Network Mismatch (HIGH)
-
-Bech32-encoded account IDs include the network. A devnet address on testnet points to a different or nonexistent account.
-
-```tsx
-// WRONG — hardcoding a bech32 address used across networks
-const ADMIN = "miden1qy35..."; // this is network-specific!
-
-// CORRECT — use hex format for cross-network compatibility
-const ADMIN = "0x1234567890abcdef";
-
-// CORRECT — derive bech32 per network
-account.bech32id(); // returns correct bech32 for current network
-```
-
-Both hex and bech32 formats work in all hooks. Prefer hex for constants, bech32 for display.
-
-## FP6: Auto-Sync Side Effects (MEDIUM)
-
-Default `autoSyncInterval` is 15000ms (15 seconds). Each sync triggers re-renders in useAccounts, useAccount, useNotes, etc.
-
-```tsx
-// PROBLEM — form resets every 15 seconds because parent re-renders
-
- {/* re-renders on every sync */}
-
-
-// SOLUTION 1 — preferred: use stable keys and memoization
-const MemoizedForm = React.memo(SendForm);
-
-// SOLUTION 2 — disable auto-sync for manual control
-
-```
-
-## FP7: IndexedDB State Loss (MEDIUM)
-
-The client persists accounts, keys, and notes in IndexedDB. Browser "Clear site data", private browsing, or storage pressure can delete everything.
-
-- Warn users that clearing browser data deletes their wallet
-- Consider external signers (Para, Turnkey) for production — keys are server-side
-- Implement account export/backup for local keystore users
-
-## FP8: Vite Configuration Requirements (MEDIUM)
-
-The `@miden-sdk/vite-plugin` package handles all Miden-specific Vite config. The recommended pattern for any new Miden app is:
-
-```ts
-import { midenVitePlugin } from "@miden-sdk/vite-plugin";
-
-export default defineConfig({
- // ST (default build): bare plugin is enough — no isolation needed
- plugins: [react(), midenVitePlugin()],
-
- // MT build only: opt into cross-origin isolation
- // plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
-});
-```
-
-`midenVitePlugin()` handles WASM loading (esnext build target, top-level await), pre-bundling exclusion (`optimizeDeps.exclude`), package deduplication, a gRPC-web RPC proxy, and — when `crossOriginIsolation: true` is passed — emits the COOP `same-origin` + COEP `require-corp` headers the **MT** build requires for `SharedArrayBuffer` on both the dev `server` and the `preview` server.
-
-| Option | Plugin source default | When to set `true` | Purpose |
-|--------|-----------------------|--------------------|---------|
-| `crossOriginIsolation` | `false` | Only when importing the MT variants (`/mt`, `/mt/lazy`) | Emit COOP/COEP headers for SharedArrayBuffer |
-
-For the **default single-threaded build**, leave `crossOriginIsolation` at its `false` default — the ST WASM loads in any browser context and needs no headers. Pass `crossOriginIsolation: true` **only** when you opt into the multi-threaded variants for local proving; without the headers the MT WASM can't construct shared memory and fails to instantiate. (The plugin README at v0.15.0 incorrectly documents the default as `true`; the executable source default is `false`, unchanged across the released tags. Do not trust the README.) The shipped example wallet uses bare `midenVitePlugin()` because it is ST (and because isolation would break the Para OAuth popups it pairs with via `paraVitePlugin()`) — see FP3. For an MT production deployment, set the same COOP/COEP headers at your real production host — the plugin only injects them into the Vite dev and preview servers. See `vite-wasm-setup` for host-specific configs.
-
-## FP9: React StrictMode Double-Init (LOW)
-
-React StrictMode double-invokes effects in development (since React 18; the React SDK's peer dep is `react >= 18.0.0`). MidenProvider guards against this, but direct low-level `createClient()` calls will initialize twice.
-
-Naming: `@miden-sdk/miden-sdk` exposes a high-level `MidenClient` wrapper class (the recommended entry point) and a low-level client re-exported as `WasmWebClient` — an `@internal` export used mainly by integration tests, whose type declaration explicitly says "Use MidenClient instead." (The class is named `WebClient` in source and re-exported under the alias `WasmWebClient`.) The React SDK does its own low-level init by importing that internal client locally as `WebClient` (`import { WasmWebClient as WebClient } from "@miden-sdk/miden-sdk"`). For manual low-level setup you would call `WasmWebClient.createClient(...)`, but prefer `MidenProvider` (or the high-level `MidenClient`) so init is guarded.
-
-```tsx
-// WRONG — manual low-level client creation in useEffect
-useEffect(() => {
- const client = await WasmWebClient.createClient(url); // called twice in dev
-}, []);
-
-// CORRECT — always use MidenProvider
-
-```
-
-## Quick Reference
-
-| # | Pitfall | Severity | One-Line Rule |
-|---|---------|----------|---------------|
-| FP1 | WASM init race | CRITICAL | Use loadingComponent or check isReady |
-| FP2 | Recursive WASM | CRITICAL | Use runExclusive() for all direct client access |
-| FP3 | COOP/COEP | HIGH | Default ST build needs no headers; required ONLY for the `/mt` build |
-| FP4 | BigInt | HIGH | Hooks accept `bigint \| number` and coerce; prefer bigint, required at the raw WASM boundary |
-| FP5 | Bech32 mismatch | HIGH | Match network in rpcUrl and addresses |
-| FP6 | Auto-sync | MEDIUM | Set autoSyncInterval: 0 if UI stability matters |
-| FP7 | IndexedDB loss | MEDIUM | Warn users; use external signers for production |
-| FP8 | Vite config | MEDIUM | Bare `midenVitePlugin()` for ST; pass `crossOriginIsolation: true` only for the `/mt` build |
-| FP9 | StrictMode | LOW | Use MidenProvider, not manual client creation |
diff --git a/skills/frontend-source-guide/SKILL.md b/skills/frontend-source-guide/SKILL.md
deleted file mode 100644
index ac6d9fb..0000000
--- a/skills/frontend-source-guide/SKILL.md
+++ /dev/null
@@ -1,174 +0,0 @@
----
-name: frontend-source-guide
-description: Guide for advanced Miden frontend development using source repo exploration. Covers AI development practices (Plan Mode, verification-driven development, context engineering, sub-agents) and maps the Miden web-sdk source repository for discovering advanced patterns. Use when building complex applications beyond basic hook usage, implementing custom signers, working with WasmWebClient directly, or troubleshooting SDK internals.
----
-
-# Advanced Miden Frontend Development: Source-Guided Context Engineering
-
-## Development Approach
-
-### 1. Plan Mode First
-
-For any non-trivial frontend application, start in Plan Mode before writing code.
-
-- Explore React SDK source and examples to understand available patterns
-- Design the component hierarchy, data flow, and which hooks to use
-- Identify which built-in hooks cover your needs vs what requires direct WasmWebClient access
-- Map out the user flow: account creation, token operations, note handling
-
-Rule of thumb: if the task involves custom transactions, external signers, or patterns not covered by the basic skills, plan first.
-
-### 2. Verification-Driven Development
-
-This is the single highest-leverage practice for AI-assisted frontend development.
-
-**Type check loop**: After every file edit, run `npx tsc -b --noEmit`. The project's type check hook does this automatically. If types fail:
-1. Read the error message
-2. Search the React SDK source for the correct type signature or hook usage
-3. Adapt the working pattern to your use case
-4. Recheck
-
-**Dev server loop**: Run `npm run dev` and check the browser. When something fails:
-1. Check the browser console for WASM errors, network errors, or React errors
-2. For WASM errors: check COOP/COEP headers and Vite config (see frontend-pitfalls skill)
-3. For unexpected behavior: compare your code against the example wallet in the React SDK
-
-Never submit code that doesn't type-check. The verification loop is your quality guarantee.
-
-### 3. Context Engineering with Source Repos
-
-The basic skills (react-sdk-patterns, frontend-pitfalls, vite-wasm-setup) cover standard patterns. For anything beyond those patterns, the web-sdk source repository is the knowledge base.
-
-**How to use source repos effectively**:
-- Don't load entire repos into context. Use sub-agents to explore — they search, read relevant files, and summarize findings without filling the main conversation context.
-- Read source files only when you need a specific answer (progressive disclosure)
-- Look for working examples first, then adapt. The example wallet app is the most reliable reference.
-- When you find a useful pattern in source, extract just what you need — the exact hook call, the exact type, the exact provider setup.
-
-**Using sub-agents for exploration**:
-- Launch an explore sub-agent with a specific question: "Find how useSwap handles the payback note type in the React SDK"
-- The sub-agent searches, reads the relevant files, and returns a focused summary
-- Your main context stays clean for implementation
-
-### 4. Iterative Frontend Development
-
-Break complex applications into stages. Complete each before starting the next:
-
-1. **Design** (Plan Mode) — Component hierarchy, data flow, hook selection
-2. **Provider setup** — MidenProvider config, signer integration if needed
-3. **Query components** — Account display, balance rendering, note lists
-4. **Mutation components** — Send forms, mint buttons, consume flows
-5. **Transaction UX** — Stage progress, error handling, loading states
-6. **Polish** — Auto-sync tuning, memoization, edge cases
-
-When stuck at any stage: search the React SDK source for a similar working pattern. Adapt it, don't guess.
-
----
-
-## Miden Source Repository Map
-
-Clone this repo alongside your project for reference. Claude will explore it when needed for advanced patterns.
-
-```bash
-# Contains the React SDK source (@miden-sdk/react), the WasmWebClient WASM bindings, and working examples
-git clone --depth 1 https://github.com/0xMiden/web-sdk.git ../web-sdk
-```
-
-### `packages/react-sdk/` — React SDK Source (`@miden-sdk/react`)
-
-The primary reference for all frontend development.
-
-- **`src/hooks/`** — All ~29 hook implementations. Each file is self-contained. Read these to understand exact parameters, error handling, and stage progression.
-- **`src/context/MidenProvider.tsx`** — Client initialization, sync loop, signer detection, runExclusive lock. Read this to understand initialization order. Note: `useMidenClient()` returns the `WasmWebClient` (aliased `WebClient`).
-- **`src/context/SignerContext.ts`** — External signer interface. Read this when implementing custom signers.
-- **`src/store/MidenStore.ts`** — Zustand store structure. Read this to understand cached state and what triggers re-renders.
-- **`src/utils/`** — Utility implementations (amounts, notes, accountBech32, runExclusive, accountParsing).
-- **`src/types/index.ts`** — All TypeScript interfaces. The single source of truth for option types, result types, and configuration.
-- **`packages/react-sdk/examples/wallet/`** — Complete working wallet app. The most reliable reference for how to set up MidenProvider, create accounts, display balances, claim notes, and send tokens.
-
-**Explore when**: Writing any new component, understanding exact hook behavior, finding how a specific feature works, debugging unexpected behavior.
-
-### `crates/web-client/` — WASM Client Bindings
-
-The Rust-to-WASM bridge that the React SDK wraps.
-
-- Contains the `WebClient` WASM struct, exported to JS as the `WasmWebClient` class (which react-sdk re-aliases to `WebClient`, the value returned by `useMidenClient()`) and all methods it exposes to JS
-- The standalone `RpcClient` struct (e.g. `getBlockHeaderByNumber`, `getNotesById`) lives here too, in `src/rpc_client/`, and is exported separately from `@miden-sdk/miden-sdk` — it is NOT reachable through `useMidenClient()`
-- JavaScript bindings in `js/` directory
-
-**Explore when**: A hook doesn't exist for your operation, understanding what WasmWebClient methods are available, debugging WASM-level errors.
-
-### `crates/idxdb-store/` — IndexedDB Persistence
-
-The browser storage layer for accounts, keys, notes, and transaction history.
-
-**Explore when**: Debugging data persistence issues, understanding what's stored in IndexedDB, investigating storage isolation for external signers.
-
----
-
-## What to Explore for Each Pattern
-
-| Building This | Explore These Paths | What to Look For |
-|---|---|---|
-| Basic wallet UI | `packages/react-sdk/examples/wallet/` | MidenProvider setup, useAccounts, useSend |
-| Custom transaction | `src/hooks/useTransaction.ts` | Request factory pattern, client methods |
-| External signer | `src/context/SignerContext.ts` | SignerContextValue interface, signCb |
-| Note consumption flow | `src/hooks/useConsume.ts` | NoteId parsing, filter construction |
-| Swap UI | `src/hooks/useSwap.ts` | Swap options, dual note types |
-| Partial swap (PSWAP) UI | `src/hooks/usePswapCreate.ts`, `usePswapConsume.ts`, `usePswapCancel.ts` | Partial-fill swap flow (new in v0.15): create, consume, cancel |
-| Token display | `src/utils/amounts.ts` | formatAssetAmount, parseAssetAmount |
-| Account ID formatting | `src/utils/accountBech32.ts` | toBech32AccountId |
-| State management | `src/store/MidenStore.ts` | Zustand selectors, cached state |
-| Direct WasmWebClient usage | `src/context/MidenProvider.tsx` | useMidenClient(), runExclusive |
-| Multi-step workflow | `src/hooks/useWaitForCommit.ts`, `useWaitForNotes.ts` | Polling, timeout patterns |
-
----
-
-## Common Advanced Patterns
-
-### Custom Hooks Wrapping WasmWebClient
-For operations not covered by built-in hooks, create custom hooks that use `useMidenClient()` and `runExclusive`. `useMidenClient()` returns the `WebClient` (WasmWebClient), so only call methods that exist on it — e.g. `getSyncHeight()`:
-```tsx
-function useSyncHeight() {
- const client = useMidenClient();
- const { runExclusive } = useMiden();
- const [height, setHeight] = useState(null);
- useEffect(() => {
- // Note: runExclusive() may be simplified in a future SDK version.
- // Check SDK changelog when upgrading.
- runExclusive(async () => {
- const h = await client.getSyncHeight();
- setHeight(h);
- });
- }, []);
- return height;
-}
-```
-
-Some operations are NOT on the `WebClient` returned by `useMidenClient()` — for example block headers. `getBlockHeaderByNumber` lives on the standalone `RpcClient` (exported from `@miden-sdk/miden-sdk`), which you construct directly with an endpoint:
-```tsx
-import { RpcClient, Endpoint } from "@miden-sdk/miden-sdk";
-
-// signature: getBlockHeaderByNumber(blockNum?: number, includeMmrProof?: boolean)
-const rpc = new RpcClient(endpoint); // endpoint: Endpoint
-const header = await rpc.getBlockHeaderByNumber(blockNumber, false);
-```
-
-### Multi-Step Workflows
-Compose hooks for complex flows (mint → wait for commit → sync → consume):
-```tsx
-const { mint } = useMint();
-const { waitForCommit } = useWaitForCommit();
-const { waitForConsumableNotes } = useWaitForNotes();
-const { consume } = useConsume();
-
-const mintAndConsume = async () => {
- const { transactionId } = await mint({ targetAccountId, faucetId, amount });
- await waitForCommit(transactionId);
- await waitForConsumableNotes({ accountId: targetAccountId });
- await consume({ accountId: targetAccountId, notes: [...] });
-};
-```
-
-### Custom Signer Implementation
-Implement the SignerContextValue interface, wrap MidenProvider in your provider. Reference `src/context/SignerContext.ts` for the exact interface contract. The `storeName` field must be unique per user to ensure IndexedDB isolation.
diff --git a/skills/idxdb-patterns/SKILL.md b/skills/idxdb-patterns/SKILL.md
deleted file mode 100644
index 62ea474..0000000
--- a/skills/idxdb-patterns/SKILL.md
+++ /dev/null
@@ -1,473 +0,0 @@
----
-name: idxdb-patterns
-description: Enforce conventions for the IndexedDB/Dexie persistence layer of the Miden web client, which lives in the web-sdk repo (idxdb-store crate). Use when editing TypeScript in `crates/idxdb-store/src/ts/`, writing Dexie transactions, or modifying the database schema.
----
-
-# IndexedDB Store Patterns (idxdb-store)
-
-This layer lives in the **web-sdk** repo (`github.com/0xMiden/web-sdk`,
-crate `crates/idxdb-store`, package `miden-idxdb-store`), not in the
-`miden-client` repo. It is a Dexie-backed `Store` implementation for the
-WASM web client.
-
-The schema splits account-related tables into `Latest…` / `Historical…`
-pairs to support account-history pruning (`client.pruneAccountHistory()`).
-Always check `crates/idxdb-store/src/ts/schema.ts` for the canonical table
-list before adding rows or filters — the active set includes `AccountAuth`,
-`AccountKeyMapping`, `Addresses`, `Settings`, `ForeignAccountCode`,
-`NotesScripts`, `TransactionScripts`, `PartialBlockchainNodes`,
-`LatestStorageMapEntries`, `HistoricalStorageMapEntries`, plus the
-account-storage / asset / account-header latest/historical pairs.
-
-## Build Workflow
-
-The `idxdb-store` has a dual-file workflow:
-
-- **TypeScript source** lives in `crates/idxdb-store/src/ts/`
-- **Generated JavaScript** lives in `crates/idxdb-store/src/js/`
-- **Both are committed to git** — the `js` folder is currently *not*
- gitignored (the `#js` entry in `src/.gitignore` is commented out)
-- The Rust side imports the generated `.js` modules via
- `#[wasm_bindgen(module = "/src/js/...")]`, so the JS must be kept in
- sync with the TS
-
-After modifying any `.ts` file, regenerate the JS with the canonical
-top-level Make target (which runs the package's `build` script through
-**pnpm** — this repo is pnpm-only, there is no yarn):
-
-```bash
-make rust-client-ts-build # == pnpm --filter web_store run build
-```
-
-The underlying package script is `tsc --build --force ./tsconfig.json`
-(`crates/idxdb-store/src/package.json`). Always commit both the `.ts`
-source and the regenerated `.js` output together.
-
-## Database Registry
-
-There is no JS object pointer on the Rust side, so open databases are
-tracked in a module-level `Map` keyed by network name, in
-`crates/idxdb-store/src/ts/schema.ts`:
-
-```typescript
-const databaseRegistry = new Map();
-
-export function getDatabase(dbId: string): MidenDatabase {
- const db = databaseRegistry.get(dbId);
- if (!db) {
- throw new Error(
- `Database not found for id: ${dbId}. Call openDatabase first.`
- );
- }
- return db;
-}
-
-export async function openDatabase(
- network: string,
- clientVersion: string
-): Promise {
- const db = new MidenDatabase(network);
- const success = await db.open(clientVersion);
- if (!success) {
- throw new Error(`Failed to open IndexedDB database: ${network}`);
- }
- databaseRegistry.set(network, db);
- return network;
-}
-```
-
-Rules:
-- Every exported store function takes `dbId: string` as its first parameter
-- Call `const db = getDatabase(dbId)` at the top of each function — look it
- up per call rather than holding a long-lived reference across calls
-- The `dbId` is the network name (`"mainnet"`, `"devnet"`, `"testnet"`, or
- a custom one); `openDatabase` registers under and returns `network`
-
-## Schema Interfaces
-
-Define TypeScript interfaces for each table with the `I` prefix. Use
-`Latest…` / `Historical…` pairs for anything that participates in account
-history (storage slots, storage map entries, vault assets, and account
-headers). The account-header pair uses `IAccount` (latest, keyed on `id`)
-and `IHistoricalAccount` (same fields plus `replacedAtNonce`) inside
-`latestAccountHeaders` / `historicalAccountHeaders`:
-
-```typescript
-export interface IAccountCode {
- root: string;
- code: Uint8Array;
-}
-
-export interface ILatestAccountStorage {
- accountId: string;
- slotName: string;
- slotValue: string;
- slotType: number;
-}
-
-export interface IHistoricalAccountStorage {
- accountId: string;
- replacedAtNonce: string;
- slotName: string;
- oldSlotValue: string | null;
- slotType: number;
-}
-
-export interface ILatestAccountAsset {
- accountId: string;
- vaultKey: string; // ASSET_KEY — see `miden-concepts` skill
- asset: string; // ASSET_VALUE serialized
-}
-
-export interface IHistoricalAccountAsset {
- accountId: string;
- replacedAtNonce: string;
- vaultKey: string;
- oldAsset: string | null;
-}
-
-export interface IAccount {
- id: string; // primary key — NOT `accountId`
- codeRoot: string;
- storageRoot: string;
- vaultRoot: string;
- nonce: string;
- committed: boolean;
- accountSeed?: Uint8Array;
- accountCommitment: string;
- locked: boolean;
- watched: boolean;
-}
-```
-
-Rules:
-- Use `string` for hex-encoded values (hashes, IDs, commitments, nonces,
- vault keys)
-- Use `Uint8Array` for raw binary data
-- Use `?` suffix for optional fields, `| null` when the column explicitly
- represents the absence of a previous value (e.g. `oldSlotValue`,
- `oldAsset`, `oldValue` in the history tables)
-- Use `boolean` for flags, `number` for block heights and slot types
-- The LATEST account-header table keys on `id`; the HISTORICAL
- account-header table keys on `accountCommitment` (with `id` and
- `[id+replacedAtNonce]` as secondary indexes). The storage / asset /
- map-entry / foreign-code tables key on `accountId`. Don't confuse the two.
-- The asset layer is two-word: `vaultKey` is the `ASSET_KEY` and `asset`
- is the encoded `ASSET_VALUE`. Don't fold them back into a single hex
- string.
-
-## Table Enum
-
-Define tables as a TypeScript enum, in the order they appear in
-`crates/idxdb-store/src/ts/schema.ts` — the Rust side imports table-backed
-JS functions verbatim:
-
-```typescript
-enum Table {
- AccountCode = "accountCode",
- LatestAccountStorage = "latestAccountStorage",
- HistoricalAccountStorage = "historicalAccountStorage",
- LatestAccountAssets = "latestAccountAssets",
- HistoricalAccountAssets = "historicalAccountAssets",
- LatestStorageMapEntries = "latestStorageMapEntries",
- HistoricalStorageMapEntries = "historicalStorageMapEntries",
- AccountAuth = "accountAuth",
- AccountKeyMapping = "accountKeyMapping",
- LatestAccountHeaders = "latestAccountHeaders",
- HistoricalAccountHeaders = "historicalAccountHeaders",
- Addresses = "addresses",
- Transactions = "transactions",
- TransactionScripts = "transactionScripts",
- InputNotes = "inputNotes",
- OutputNotes = "outputNotes",
- NotesScripts = "notesScripts",
- StateSync = "stateSync",
- BlockHeaders = "blockHeaders",
- PartialBlockchainNodes = "partialBlockchainNodes",
- Tags = "tags",
- ForeignAccountCode = "foreignAccountCode",
- Settings = "settings",
-}
-```
-
-The Dexie store schema is defined once, as the `V1_STORES` constant
-applied via `this.dexie.version(1).stores(V1_STORES)` in the
-`MidenDatabase` constructor. `V1_STORES` is the frozen baseline: index
-strings are built with a small `indexes(...)` helper, e.g.
-`[Table.LatestAccountStorage]: indexes("[accountId+slotName]", "accountId")`.
-
-The migration system is **not currently in use** — the Miden network
-resets on every upgrade, so `ensureClientVersion` nukes the DB (close /
-`delete` / re-open) when the running client version is a higher major or
-minor than the stored one; same-major.minor patch bumps and downgrades
-just persist the new version without resetting (see the semver
-`sameMajorMinor` / `!semver.gt(...)` guard in `ensureClientVersion`).
-A minor-version bump does trigger it. Adding a table or
-changing an index today therefore means:
-1. Update the `Table` enum + interface(s) in `schema.ts`
-2. Add the table/index to `V1_STORES` (additive, since the DB is nuked on
- version change; once migrations are enabled, `V1_STORES` must be frozen
- and a new `.version(N+1).stores({...}).upgrade(...)` block added instead)
-3. Update Rust-side reads/writes, which import the corresponding JS
- functions through `#[wasm_bindgen(module = "/src/js/.js")]`
- (e.g. account functions from `/src/js/accounts.js`, schema/registry
- functions from `/src/js/schema.js`)
-4. Run `make rust-client-ts-build` to regenerate the JS, and add a
- schema/migration test in `schema.test.ts`
-
-## Dexie Transactions
-
-### Atomic Operations
-
-When multiple tables must be updated together, wrap in a Dexie transaction.
-List every table the transaction touches, and use `Promise.all()` to run
-independent operations concurrently (from `applyStateSync` in
-`crates/idxdb-store/src/ts/sync.ts`):
-
-```typescript
-const tablesToAccess = [
- db.stateSync,
- db.inputNotes,
- db.outputNotes,
- db.notesScripts,
- db.transactions,
- db.transactionScripts,
- db.blockHeaders,
- db.partialBlockchainNodes,
- db.tags,
- db.latestAccountHeaders,
- db.historicalAccountHeaders,
- // ... plus the latest/historical storage, map-entry and asset tables
-];
-
-return await db.dexie.transaction("rw", tablesToAccess, async (tx) => {
- await Promise.all([
- /* input/output note upserts */,
- /* transaction upserts */,
- /* per-account applyFullAccountState calls */,
- updateSyncHeight(tx, blockNum),
- updatePartialBlockchainNodes(tx, serializedNodeIds, serializedNodes),
- updateCommittedNoteTags(tx, committedNoteTagSources),
- /* block-header writes */,
- ]);
-});
-```
-
-Rules:
-- Use `"rw"` for read-write transactions
-- List all tables that will be accessed in the `tablesToAccess` array
-- Use `Promise.all()` inside transactions to parallelize independent operations
-- Pass the `tx` transaction object to helper functions that need table access
-- Helper write functions (e.g. `upsertInputNote`) commonly take an optional
- `tx?: Transaction`: if supplied they run inside the caller's transaction,
- otherwise they open their own `db.dexie.transaction(...)`
-
-### Table Access Within Transactions
-
-The Dexie `Transaction` type doesn't statically declare table accessors.
-`schema.ts` augments `declare module "dexie"` so `tx.inputNotes` etc.
-type-check; where that augmentation isn't in scope, type-cast the
-transaction (from `updateSyncHeight` in `sync.ts`):
-
-```typescript
-async function updateSyncHeight(tx: Transaction, blockNum: number) {
- try {
- const current = await (
- tx as Transaction & { stateSync: Dexie.Table }
- ).stateSync.get(1);
- if (!current || current.blockNum < blockNum) {
- await (
- tx as Transaction & { stateSync: Dexie.Table }
- ).stateSync.update(1, { blockNum: blockNum });
- }
- } catch (error) {
- logWebStoreError(error, "Failed to update sync height");
- }
-}
-```
-
-### Forward-Only Updates
-
-Only advance the sync height forward (never regress):
-
-```typescript
-if (!current || current.blockNum < blockNum) {
- // Update
-}
-```
-
-## Error Handling
-
-### logWebStoreError
-
-Use `logWebStoreError()` from `./utils.js` for error logging — it formats
-Dexie errors (and walks `error.inner`), then **re-throws** the error:
-
-```typescript
-import { logWebStoreError } from "./utils.js";
-
-try {
- // database operation
-} catch (error) {
- logWebStoreError(error, "Error while fetching account headers");
-}
-```
-
-Because `logWebStoreError` always re-throws, code after a `catch` that
-calls it (e.g. a trailing `return []`) is effectively unreachable on the
-error path — the surrounding `try` body must return the success value.
-
-### Reads return optional / empty
-
-Read functions wrap their body in `try/catch`, returning the queried value
-on success. The fallback after the catch (empty array, `null`, or
-`undefined`) documents intent but is unreachable because `logWebStoreError`
-re-throws (from `getAccountIds` in `crates/idxdb-store/src/ts/accounts.ts`):
-
-```typescript
-export async function getAccountIds(dbId: string) {
- try {
- const db = getDatabase(dbId);
- const records = await db.latestAccountHeaders.toArray();
- return records.map((entry) => entry.id); // header rows key on `id`
- } catch (error) {
- logWebStoreError(error, "Error while fetching account IDs");
- }
- return [];
-}
-```
-
-## Data Operations
-
-### Querying
-
-Use Dexie's query API. Patterns actually used in the store:
-
-```typescript
-// Get all records
-const records = await db.latestAccountHeaders.toArray();
-
-// Get by primary key (e.g. stateSync row id 1)
-const current = await db.stateSync.get(1);
-
-// Look up a header by its `id` index (header PK is `id`)
-const record = await db.latestAccountHeaders
- .where("id")
- .equals(accountId)
- .first();
-
-// Filter a single-field index, optionally narrowing with `.and(...)`
-const slots = await db.latestAccountStorages
- .where("accountId")
- .equals(accountId)
- .and((record) => nameSet.has(record.slotName))
- .toArray();
-
-// Match multiple keys against one index
-const codes = await db.accountCodes.where("root").anyOf(codeRoots).toArray();
-```
-
-For compound indexes, use the **bracket-string** index name and pass the
-key parts as an array to `.equals(...)` (from `applyTransactionDelta`):
-
-```typescript
-const oldSlot = await db.latestAccountStorages
- .where("[accountId+slotName]")
- .equals([accountId, slot.slotName])
- .first();
-```
-
-### Latest vs Historical
-
-For account state, the `latest…` tables hold the current row (keyed by
-`accountId`, or the compound `[accountId+slotName]` / `[accountId+vaultKey]`
-/ `[accountId+slotName+key]`); the matching `historical…` tables hold the
-value that was replaced, keyed by `[accountId+replacedAtNonce…]` with the
-prior value in `oldSlotValue` / `oldAsset` / `oldValue` (`null` when no
-previous value existed). The write path is **archive-then-replace**: read
-the current latest row, `put` it into historical under the new nonce, then
-`put` the new value into latest (see `applyTransactionDelta` /
-`applyFullAccountState`).
-
-Undo restores from history back to latest, keyed by the compound nonce
-index; a non-null old value overwrites latest, a `null` old value deletes
-the latest row (from `restoreSlotsFromHistorical` in `accounts.ts`):
-
-```typescript
-const oldSlots = await db.historicalAccountStorages
- .where("[accountId+replacedAtNonce]")
- .equals([accountId, nonce])
- .toArray();
-
-for (const slot of oldSlots) {
- if (slot.oldSlotValue !== null) {
- await db.latestAccountStorages.put({ /* ...restore old value... */ });
- } else {
- await db.latestAccountStorages
- .where("[accountId+slotName]")
- .equals([accountId, slot.slotName])
- .delete();
- }
-}
-```
-
-`client.pruneAccountHistory()` (web-client `pruneAccountHistory`, backed by
-the JS `pruneAccountHistory` in `accounts.ts`) drops `historical…` rows
-whose `replacedAtNonce <= upToNonce` and any orphaned account code. Write
-functions must keep the latest row authoritative regardless of how much
-history has been pruned.
-
-### Serialization Conventions
-
-- Hex strings for cryptographic values (hashes, IDs, commitments, vault keys)
-- `uint8ArrayToBase64()` (from `./utils.js`) when a `Uint8Array` must be
- returned to Rust as a base64 string (e.g. serialized account code, seeds)
-- `Uint8Array` for direct binary storage in a table column
-- Default empty strings for optional string fields when reading out:
- `record.storageRoot || ""`
-- `BigInt()` for nonce comparisons and sorting (nonces are stored as
- strings, so lexicographic / index-range ordering would be wrong)
-
-## Upsert Pattern
-
-Dexie `Table.put()` is itself an upsert: it inserts or replaces by primary
-key. The store builds a plain data object and calls `.put()` — it does not
-read-then-branch. Convert `null` to `undefined` so Dexie omits the field
-from indexes (a `null` in a compound index is a real value; an absent
-field is skipped). From `upsertInputNote` in
-`crates/idxdb-store/src/ts/notes.ts`:
-
-```typescript
-export async function upsertInputNote(
- dbId: string,
- detailsCommitment: string,
- noteId: string | undefined,
- // ... more params
- consumedBlockHeight?: number | null,
- consumedTxOrder?: number | null,
- consumerAccountId?: string | null,
- tx?: Transaction
-) {
- const db = getDatabase(dbId);
- const doWork = async (t: Transaction) => {
- try {
- const data = {
- detailsCommitment,
- noteId: noteId ?? undefined,
- // null -> undefined so Dexie omits these from compound indexes
- consumedBlockHeight: consumedBlockHeight ?? undefined,
- consumedTxOrder: consumedTxOrder ?? undefined,
- consumerAccountId: consumerAccountId ?? undefined,
- // ... remaining fields
- };
- await t.inputNotes.put(data);
- await t.notesScripts.put({ scriptRoot, serializedNoteScript });
- } catch (error) {
- logWebStoreError(error, `Error inserting note: ${detailsCommitment}`);
- }
- };
- // Run inside the caller's tx if provided, else open one.
- if (tx) return doWork(tx);
- return db.dexie.transaction("rw", db.inputNotes, db.notesScripts, doWork);
-}
-```
diff --git a/skills/react-sdk-patterns/SKILL.md b/skills/react-sdk-patterns/SKILL.md
deleted file mode 100644
index f016e54..0000000
--- a/skills/react-sdk-patterns/SKILL.md
+++ /dev/null
@@ -1,386 +0,0 @@
----
-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` (exported as `WebClient`) via `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
-
-```tsx
-import { MidenProvider } from "@miden-sdk/react";
-
-} // shown during WASM init
- errorComponent={(error) => } // function form receives the Error; a static element does not
->
-
-
-```
-
-| 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()
-```tsx
-const { accounts, wallets, faucets, isLoading, error, refetch } = useAccounts();
-// accounts — AccountHeader[] (every tracked account)
-// wallets — mirrors `accounts` (faucet-vs-wallet is not encoded in the account id)
-// faucets — always `[]`
-```
-
-An account's faucet-vs-wallet kind is not encoded in the account id, so `wallets` mirrors `accounts` and `faucets` is always empty. Use `accounts` and detect faucets **per-account** via `account.isFaucet()` (load the full `Account` with `useAccount`).
-
-### useAccount(accountId: string)
-```tsx
-const { account, assets, getBalance, isLoading, error, refetch } = useAccount(accountId);
-// account — Account object (.id(), .nonce(), .bech32id(), .isFaucet())
-// assets — AssetBalance[] (assetId, amount, symbol?, decimals?)
-// getBalance(faucetId) — bigint balance for specific token
-```
-
-`account.id()` and `account.nonce()` are methods (call them, then `.toString()` to render). `bech32id()` is installed on the `Account` prototype by the React SDK.
-
-### useNotes(filter?)
-```tsx
-const { notes, consumableNotes, noteSummaries, consumableNoteSummaries, isLoading, error, refetch } = useNotes();
-// notes — InputNoteRecord[] (filtered ONLY by `status`)
-// consumableNotes — ConsumableNoteRecord[] (filtered ONLY by `accountId`)
-// noteSummaries — NoteSummary[] (id, assets, sender) — also filtered by `sender` and `excludeIds`
-// consumableNoteSummaries — NoteSummary[] — also filtered by `sender` and `excludeIds`
-
-// Each filter option only narrows specific fields — destructure the one it affects:
-
-// `status` filters the returned `notes` (the only option that does):
-const { notes } = useNotes({ status: "committed" }); // "all" | "consumed" | "committed" | "expected" | "processing"
-// `accountId` filters `consumableNotes` (NOT `notes`):
-const { consumableNotes } = useNotes({ accountId: "0x..." });
-// `sender` filters only the summary arrays (NOT `notes`/`consumableNotes`):
-const { noteSummaries, consumableNoteSummaries } = useNotes({ sender: "0x..." });
-// `excludeIds` filters only the summary arrays:
-const { noteSummaries, consumableNoteSummaries } = useNotes({ excludeIds: ["0xnote1", "0xnote2"] });
-```
-
-### useNoteStream(options?)
-```tsx
-const { notes, latest, markHandled, markAllHandled, snapshot, isLoading, error } = useNoteStream();
-// notes — StreamedNote[] (matching filter criteria)
-// latest — most recent StreamedNote (convenience)
-// markHandled(noteId) — exclude a note from future renders
-// markAllHandled() — exclude all current notes
-// snapshot() — capture { ids, timestamp } for cross-phase filtering
-
-// Options:
-const { notes } = useNoteStream({ status: "committed", sender: "0x..." });
-const { notes } = useNoteStream({ since: Date.now() - 60000 }); // last 60s
-const { notes } = useNoteStream({ excludeIds: new Set(["0xnote1"]) });
-const { notes } = useNoteStream({ amountFilter: (amount) => amount > 100n });
-```
-
-### useSyncState()
-```tsx
-const { syncHeight, isSyncing, lastSyncTime, sync, error } = useSyncState();
-await sync(); // Manual sync
-```
-
-### useAssetMetadata(assetIds?: string[])
-```tsx
-const { assetMetadata } = useAssetMetadata([faucetId]); // takes a string[] (NOT a bare string)
-// assetMetadata — Map
-// Each entry: { assetId, symbol?, decimals? }
-const meta = assetMetadata.get(faucetId);
-// meta.symbol — "TEST"
-// meta.decimals — 8
-```
-
-Pass an array even for a single asset — the hook calls `.filter` on its argument, so a bare string throws a runtime `TypeError`.
-
-### useTransactionHistory(options?)
-```tsx
-const { records, record, status, isLoading, error, refetch } = useTransactionHistory({ id: txId });
-// status: "pending" | "committed" | "discarded" | null
-```
-
-## Mutation Hooks
-
-Each returns its own action function plus `error` and `reset`. The two families differ in their loading/progress fields:
-- **Transaction hooks** (`useSend`, `useMultiSend`, `useMint`, `useConsume`, `useSwap`, `useTransaction`) expose `isLoading` and `stage` (a `TransactionStage`).
-- **Account create/import hooks** (`useCreateWallet`, `useCreateFaucet`, `useImportAccount`) expose `isCreating` (or `isImporting` for the latter) and have **no** `stage`.
-
-**Transaction stages**: `"idle"` → `"executing"` → `"proving"` → `"submitting"` → `"complete"`
-
-Auth scheme for the create/import hooks. The `AuthScheme` re-exported from the package root is the friendly string const `{ Falcon: "falcon", ECDSA: "ecdsa" }`:
-
-```tsx
-import { AuthScheme } from "@miden-sdk/react";
-// AuthScheme.Falcon === "falcon" | AuthScheme.ECDSA === "ecdsa"
-```
-
-> **Known issue ([web-sdk#223](https://github.com/0xMiden/web-sdk/issues/223)):** `useCreateWallet` / `useCreateFaucet` / `useImportAccount` forward `authScheme` straight to the low-level `WebClient.newWallet`, which currently expects the **numeric** wasm enum (`AuthRpoFalcon512 = 2`, `AuthEcdsaK256Keccak = 1`), not the friendly string, and the default resolves to `undefined` (which hangs the call). Until it is fixed, pass the numeric value: `authScheme: 2` (Falcon) or `authScheme: 1` (ECDSA). The examples below use `2`.
-
-### useCreateWallet()
-```tsx
-const { createWallet, wallet, isCreating, error, reset } = useCreateWallet();
-const account = await createWallet({
- storageMode: "private", // "private" | "public". Default: "private"
- authScheme: 2, // 2 = Falcon; friendly AuthScheme.* not accepted here yet (web-sdk#223)
- initSeed: seedBytes, // optional: Uint8Array for a deterministic account id
-});
-```
-
-### useCreateFaucet()
-```tsx
-const { createFaucet, faucet, isCreating, error, reset } = useCreateFaucet();
-const account = await createFaucet({
- tokenSymbol: "TEST",
- tokenName: "Test Token", // optional: defaults to tokenSymbol
- decimals: 8, // Default: 8
- maxSupply: 1000000n, // bigint | number
- storageMode: "private", // "private" | "public". Default: "private"
- authScheme: 2, // 2 = Falcon; friendly AuthScheme.* not accepted here yet (web-sdk#223)
-});
-```
-
-### useImportAccount()
-```tsx
-const { importAccount, account, isImporting, error, reset } = useImportAccount();
-
-// Import by account ID (network lookup):
-const account = await importAccount({ type: "id", accountId: "0x..." });
-
-// Import from file:
-const account = await importAccount({ type: "file", file: accountFileOrBytes });
-
-// Import from seed:
-const account = await importAccount({
- type: "seed",
- seed: seedBytes,
- authScheme: 2, // optional; 2 = Falcon (web-sdk#223 — friendly AuthScheme.* not accepted here yet)
-});
-```
-
-### useSend()
-```tsx
-const { send, result, isLoading, stage, error, reset } = useSend();
-await send({
- from: senderAccountId,
- to: recipientAccountId,
- assetId: faucetId, // token faucet ID
- amount: 1000n, // bigint!
- noteType: "private", // "private" | "public". Default: "private"
- recallHeight: 100, // optional: sender can reclaim after this block
- timelockHeight: 50, // optional: recipient can consume after this block
- sendAll: true, // optional: send entire balance (ignores amount)
- attachment: [1n, 2n], // optional: arbitrary data attached to the note
-});
-```
-
-### useMultiSend()
-```tsx
-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" }, // per-recipient override
- { to: recipient3, amount: 200n, attachment: [1n, 2n, 3n] }, // per-recipient attachment
- ],
- noteType: "private", // default for all recipients
-});
-```
-
-### useMint()
-```tsx
-const { mint, result, isLoading, stage, error, reset } = useMint();
-await mint({
- targetAccountId: recipientId,
- faucetId: myFaucetId,
- amount: 10000n, // bigint!
- noteType: "public",
-});
-```
-
-### useConsume()
-```tsx
-const { consume, result, isLoading, stage, error, reset } = useConsume();
-await consume({
- accountId: myAccountId,
- notes: [noteId1, noteId2], // accepts: hex string IDs, NoteId, InputNoteRecord, or Note
-});
-```
-
-### useSwap()
-```tsx
-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
-```tsx
-const { execute, result, isLoading, stage, error, reset } = useTransaction();
-
-// With pre-built TransactionRequest:
-await execute({ accountId, request: txRequest });
-
-// With factory function (gets access to client):
-await execute({
- accountId,
- request: (client) => client.newSwapTransactionRequest(/* ... */),
-});
-```
-
-### useWaitForCommit()
-```tsx
-const { waitForCommit } = useWaitForCommit();
-await waitForCommit(result.txId, { // useSend returns { txId, note }; other hooks use { transactionId }
- timeoutMs: 10000, // Default: 10000
- intervalMs: 1000, // Default: 1000
-});
-```
-
-### useWaitForNotes()
-```tsx
-const { waitForConsumableNotes } = useWaitForNotes();
-await waitForConsumableNotes({
- accountId: myAccountId,
- minCount: 1, // Default: 1
- timeoutMs: 10000,
-});
-```
-
-### useSessionAccount(options)
-```tsx
-const { initialize, sessionAccountId, isReady, step, error, reset } = useSessionAccount({
- fund: async (sessionId) => {
- // Called after session wallet is created — fund it here
- await send({ from: mainWallet, to: sessionId, assetId: faucetId, amount: 100n });
- },
- assetId: faucetId, // optional: for note filtering
- walletOptions: { // optional: session wallet creation options
- storageMode: "private", // "private" | "public"
- authScheme: 2, // 2 = Falcon (web-sdk#223)
- },
- pollIntervalMs: 3000, // optional: funding detection interval. Default: 3000
-});
-// Steps: "idle" → "creating" → "funding" → "consuming" → "ready"
-// Call initialize() to start the flow. isReady becomes true when fully funded.
-```
-
-## Transaction Progress UI
-
-```tsx
-function SendButton({ from, to, assetId, amount }) {
- const { send, stage, isLoading, error } = useSend();
-
- return (
-
-
- {error &&
Error: {error.message}
}
-
- );
-}
-```
-
-## 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/use-miden-para-react` — EVM wallets
-- `TurnkeySignerProvider` from `@miden-sdk/miden-turnkey-react` — passkey auth
-- `MidenFiSignerProvider` from `@miden-sdk/miden-wallet-adapter-react` — MidenFi wallet
-
-These three packages live in external repos (not in web-sdk), so confirm the exact published names against the current Para/Turnkey/MidenFi integration docs before installing. The v0.15 example app (`packages/react-sdk/examples/wallet/src/main.tsx`) imports them as above; some web-sdk docs alias the Para package as `@miden-sdk/para`.
-
-```tsx
-// Example: Para signer wrapping MidenProvider
-import { ParaSignerProvider } from "@miden-sdk/use-miden-para-react";
-
-
-
-```
-
-### useSigner() — Unified Interface
-Returns `SignerContextValue | null` — `null` in local-keystore mode (no signer provider mounted). Guard before destructuring.
-```tsx
-const signer = useSigner();
-if (!signer) return null; // local keystore mode
-const { isConnected, connect, disconnect, name } = signer;
-```
-
-### Custom Signer
-Implement `SignerContextValue` interface via `SignerContext.Provider`. Requires: `name`, `storeName` (unique per user for DB isolation), `accountConfig`, `signCb`, `isConnected`, `connect`, `disconnect`. See `frontend-source-guide` skill for source references.
-
-## Utility Functions
-
-```tsx
-import { formatAssetAmount, parseAssetAmount, getNoteSummary, formatNoteSummary, toBech32AccountId } from "@miden-sdk/react";
-
-formatAssetAmount(1000000n, 8) // "0.01"
-parseAssetAmount("0.01", 8) // 1000000n
-const summary = getNoteSummary(note); // { id, assets, sender }
-formatNoteSummary(summary); // "1.5 TEST from mtst1..." (the " from " suffix is appended whenever the summary has a sender)
-toBech32AccountId("0x1234..."); // "mtst1..." (testnet HRP; defaults to testnet)
-```
-
-The HRP is inferred from the configured `rpcUrl` and defaults to testnet: mainnet=`mm`, testnet=`mtst` (default), devnet=`mdev` — there is no `miden` HRP.
-
-## Direct Client Access
-
-```tsx
-const client = useMidenClient(); // throws if not ready
-const { runExclusive } = useMiden();
-
-// For operations not covered by hooks (use methods on the WebClient itself —
-// e.g. getSyncHeight, getAccount, getTransactions; getBlockHeaderByNumber lives on RpcClient, not here):
-await runExclusive(async () => {
- const height = await client.getSyncHeight();
-});
-```
-
-## Type Imports
-
-```tsx
-import { AuthScheme } from "@miden-sdk/react"; // value (friendly string const { Falcon, ECDSA }), not just a type
-
-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";
-```
diff --git a/skills/signer-integration/SKILL.md b/skills/signer-integration/SKILL.md
deleted file mode 100644
index 5d774b2..0000000
--- a/skills/signer-integration/SKILL.md
+++ /dev/null
@@ -1,196 +0,0 @@
----
-name: signer-integration
-description: Guide to integrating external signers (Para, Turnkey, MidenFi wallet adapter) and building custom signers for Miden React frontends. Covers provider setup, passkey authentication, unified signer interface, custom SignerContext implementation, and custom account components. Use when adding wallet connection, authentication, or external key management to a Miden frontend.
----
-
-# Miden Signer Integration
-
-## Overview
-
-By default, MidenProvider uses a **local keystore** (keys in IndexedDB, no wallet connection needed). For production apps, wrap MidenProvider with a signer provider to use external key management.
-
-Signer providers must wrap MidenProvider (outer → inner):
-```
- ← manages keys + auth
- ← manages Miden client
-
-
-
-```
-
-## Pre-Built Signer Providers
-
-### Para (EVM Wallets)
-```tsx
-import { ParaSignerProvider, useParaSigner } from "@miden-sdk/use-miden-para-react";
-
-
-
-
-
-
-
-const { para, wallet, isConnected } = useParaSigner();
-```
-
-### Turnkey (Passkey Authentication)
-```tsx
-import { TurnkeySignerProvider } from "@miden-sdk/miden-turnkey-react";
-
-// `config` is REQUIRED, and `defaultOrganizationId` is required within it.
-// Type: Pick
-// & Partial>
-// — only the other fields (e.g. `apiBaseUrl`) are optional; `apiBaseUrl`
-// defaults to https://api.turnkey.com. There is NO env-var fallback for the
-// org id (the provider does not read `VITE_TURNKEY_ORG_ID`).
-
-
-
-
-
-
-// Or override the apiBaseUrl default:
-
- ...
-
-```
-
-`TurnkeySignerProvider` also accepts optional `customComponents` and `importAccountId` props, which it forwards into `accountConfig` (see "Custom Account Components").
-
-Connect via passkey:
-```tsx
-import { useSigner } from "@miden-sdk/react";
-import { useTurnkeySigner } from "@miden-sdk/miden-turnkey-react";
-
-// useSigner() returns null in local-keystore mode (no signer provider mounted),
-// so guard before destructuring.
-const signer = useSigner();
-if (!signer) return null;
-const { isConnected, connect, disconnect } = signer;
-await connect(); // triggers passkey flow, auto-selects account
-
-// Turnkey-specific extras
-const { client, account, setAccount } = useTurnkeySigner();
-```
-
-### MidenFi Wallet Adapter (Browser Extension)
-```tsx
-import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react";
-import { WalletAdapterNetwork } from "@miden-sdk/miden-wallet-adapter-base";
-
-
-
-
-
-
-```
-
-With `MidenFiSignerProvider` in place, use `useSigner()` from the React SDK to manage connection state. The regular React SDK hooks (`useSend`, `useConsume`, etc.) automatically sign via the connected wallet — no additional wiring needed.
-
-> The provider accepts an `accountType` prop, but it is a no-op: account visibility is determined solely by `storageMode` (`private`/`public`), and the provider always imports the account by ID (`importAccountId`), bypassing the builder path entirely. Omit it.
-
-### Frontend-template-specific MidenFi pattern
-
-The [frontend template](https://github.com/0xMiden/frontend-template) (on web-sdk 0.15 — `@miden-sdk/miden-sdk@0.15.3`, `@miden-sdk/react@0.15.3`, wallet adapters `0.15.1`) deviates from the generic patterns above in three places worth knowing when the wallet extension is the primary signer:
-
-- **Provider order is INVERTED: `MidenProvider` runs OUTSIDE `MidenFiSignerProvider`** — see `src/providers.tsx`. This is the opposite of the canonical signer-outer / Miden-inner nesting at the top of this skill, and it is deliberate. In v0.15, when a signer provider is an *ancestor* of `MidenProvider`, `MidenProvider` treats it as its external keystore and does NOT create the `WebClient` until the signer connects (the init effect sees `signerIsConnected === false` and returns early before building the client). With a wallet that hasn't connected — or any environment without the extension — the app would hang on "Initializing…" and even public reads couldn't run. The template never signs *through* `MidenProvider` (it signs its only write, the counter increment, through the local `WebClient` rather than the wallet), so it runs `MidenProvider` in local-keystore mode (no signer ancestor → it initializes immediately, reads work pre-connect) and keeps `MidenFiSignerProvider` *inside*, purely for the connect button and the wallet's `requestTransaction`. `MidenFiSignerProvider` works standalone (it provides its own `WalletContext` + `SignerContext`; no `MultiSignerProvider` needed). Use this inversion only when you do not sign through `MidenProvider`; if external-keystore signing IS the goal, keep the canonical signer-outer order so `MidenProvider` picks up the signer's `signCb`/`accountConfig`.
-- **Wallet button uses `useMidenFiWallet()` + `WalletReadyState`** — see `src/components/AppContent.tsx`. The button gates on `wallet?.readyState` (rendering a disabled "Install MidenFi Wallet" state unless `readyState` is `Installed` or `Loadable`) so it can show install state before the extension is detected. `useSigner().connect()` would silently fall through to the adapter's `window.open(adapter.url, ...)` install fallback; gating on `readyState` avoids that path.
-- **The counter increment is a local two-transaction flow, not a wallet-signed tx** — see `src/hooks/useIncrementCounter.ts`. It does not use the wallet at all. It creates a throwaway local sender (`client.newWallet(...)`), publishes a plain increment note as that sender's own output note (`TransactionRequestBuilder().withOwnOutputNotes(...)`), then consumes the note *as the counter* (`client.newConsumeTransactionRequest([note])`). Both transactions are submitted by the local `WebClient` via `submitNewTransactionWithProver(accountId, request, prover)` (remote prover), never by the wallet, so `useWaitForCommit` doesn't apply and the template polls the counter's storage map instead. This mirrors the project-template `increment_count` reference.
- - **The note APIs in that hook (use as the reference):** the JS `NoteMetadata` constructor is attachment-less — `new NoteMetadata(sender, noteType, tag)`. Build the note with `new Note(new NoteAssets(), metadata, recipient)`. The increment note carries no attachment and uses tag `0`; the counter is a plain **public `NoAuth`** account, so anyone can consume the note against it with no signature. (Attachments still exist for other uses — `NoteAttachment.fromWord(scheme, word)` / `fromWords(scheme, words)`, read back via `.toWords()`, or `createNoteAttachment(...)` — but the increment does not need one. v0.15 removed the network-account model, so there is no network-execution targeting.)
- - **Two hard requirements (don't regress):** (1) the client runs with `useWorker: false` on `MidenProvider`. The default worker shim keeps a separate in-memory SMT forest per thread; consuming against an *imported* (not locally-created) account applies a delta transaction whose apply step looks the account up in the executing (worker) forest, which never contains the late-imported counter, so it fails with `account data wasn't found` ([web-sdk#222](https://github.com/0xMiden/web-sdk/issues/222)). One thread means one forest, which fixes it. (2) Submits go through the remote prover (`submitNewTransactionWithProver`) so the worker-less single thread only pays local execution, not minutes of local proving. The increment works end-to-end on v0.15 (verified on testnet); there is no `INCREMENT_ONCHAIN_BLOCKED` flag.
-
-## Unified Signer Interface
-
-Works with any signer provider above. `useSigner()` returns `null` in local-keystore mode (no signer provider mounted), so guard before destructuring:
-```tsx
-import { useSigner } from "@miden-sdk/react";
-
-const signer = useSigner();
-if (!signer) return null; // local keystore mode — no external signer
-
-const { isConnected, connect, disconnect, name } = signer;
-
-if (!isConnected) {
- return ;
-}
-```
-
-## Building a Custom Signer
-
-Implement `SignerContextValue` via `SignerContext.Provider`:
-
-```tsx
-import { SignerContext } from "@miden-sdk/react";
-import { AccountStorageMode } from "@miden-sdk/miden-sdk";
-
- {
- // Route to your signing service
- return signature; // Uint8Array
- },
- connect: async () => { /* trigger wallet connection */ },
- disconnect: async () => { /* clear session */ },
-}}>
-
-
-
-
-```
-
-**Required fields:**
-- `name` — Display name for the signer
-- `storeName` — Unique string per user (isolates IndexedDB data between users)
-- `accountConfig` — `{ publicKeyCommitment: Uint8Array; storageMode: AccountStorageMode; ... }` (storage mode is an `AccountStorageMode` instance, e.g. `AccountStorageMode.private()`, not a string)
-- `signCb` — Callback that signs transaction data with your key management service
-- `connect` / `disconnect` — Session lifecycle handlers
-
-## Custom Account Components
-
-Attach application-specific `AccountComponent` instances (e.g., DEX logic from `.masp` packages) to accounts created by the signer:
-
-```tsx
-import { type SignerAccountConfig } from "@miden-sdk/react";
-import { AccountComponent } from "@miden-sdk/miden-sdk";
-
-const myDexComponent: AccountComponent = await loadCompiledComponent();
-
-const accountConfig: SignerAccountConfig = {
- publicKeyCommitment: userPublicKeyCommitment,
- storageMode: myStorageMode, // an AccountStorageMode instance (e.g. AccountStorageMode.public())
- customComponents: [myDexComponent],
-};
-```
-
-`SignerAccountConfig` has an `accountType` field, but it is ignored — account kind and code mutability are not encoded in the account, so visibility comes solely from `storageMode`. Omit it.
-
-Components are appended to the `AccountBuilder` after the default basic wallet component. The field is optional — omitting it preserves default behavior.
-
-## Which Signer to Choose
-
-| Signer | Auth Method | Keys Stored | Best For |
-|--------|-------------|-------------|----------|
-| Local keystore (default) | None | Browser IndexedDB | Development, demos |
-| Para | EVM wallet | Para servers | Apps with existing EVM users |
-| Turnkey | Passkey (biometric) | Turnkey servers | Consumer apps, no seed phrases |
-| MidenFi Wallet | Browser extension | Extension | Power users with MidenFi wallet |
-| Custom | Your choice | Your infrastructure | Enterprise, custom auth flows |
-
-**Key trade-off**: Local keystore requires no setup but keys are lost if the user clears browser data. External signers persist keys server-side but add a dependency.
diff --git a/skills/testing-patterns/SKILL.md b/skills/testing-patterns/SKILL.md
deleted file mode 100644
index b717283..0000000
--- a/skills/testing-patterns/SKILL.md
+++ /dev/null
@@ -1,237 +0,0 @@
----
-name: testing-patterns
-description: Testing conventions, mock factory, fixtures, and TDD workflow for Miden frontend development. Covers Vitest + testing-library setup, @miden-sdk/react module mocking, realistic fixture data, test patterns for query and mutation hooks, and the automated verification pipeline. Use when writing, running, or debugging tests for Miden React components.
----
-
-# Miden Frontend Testing Patterns
-
-## Test Stack
-
-- **Vitest** — Test runner (extends Vite config for consistent behavior)
-- **@testing-library/react** — Component rendering and queries
-- **@testing-library/user-event** — User interaction simulation
-- **@testing-library/jest-dom** — DOM assertion matchers (toBeInTheDocument, toBeDisabled, etc.)
-- **jsdom** — Browser environment for tests
-
-## Mock Factory: `@miden-sdk/react`
-
-All Miden SDK hooks are mocked via `src/__tests__/mocks/miden-sdk-react.ts`. This module exports mock implementations of every hook with realistic default return values.
-
-### Usage in test files
-
-```tsx
-// 1. Mock the entire module (hoisted to top by vitest)
-vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
-
-// 2. Import hooks you want to override
-import { useAccounts, useSend } from "@miden-sdk/react";
-
-// 3. Override per-test
-it("shows empty state", () => {
- vi.mocked(useAccounts).mockReturnValue({
- accounts: [],
- wallets: [],
- faucets: [],
- isLoading: false,
- error: null,
- refetch: vi.fn(),
- });
- render();
-});
-```
-
-### Default mock return values
-
-**Query hooks** return populated data by default:
-- `useAccounts()` — default mock returns `accounts` (3 headers), `wallets` (2 wallet headers), and `faucets` (1 faucet header). The template mock intentionally keeps the `wallets`/`faucets` split populated so the query-hook pattern can exercise both lists. NOTE: the real v0.15 hook deprecates these fields — it returns `wallets: accounts` and `faucets: []` (protocol 0.15 removed faucet-vs-wallet from the account id, so accounts can't be split from headers alone); detect faucet-ness per-account from its components, not from a `faucets` array. The override example above (`wallets: [], faucets: []`) is a valid manual override but is NOT the default mock.
-- `useAccount()` — account with 10.0 TEST token balance
-- `useNotes()` — 1 input note, 1 consumable note
-- `useSyncState()` — syncHeight: 12345, not syncing
-- `useAssetMetadata()` — TEST token metadata (symbol, decimals: 8)
-- `useMiden()` — isReady: true
-
-**Mutation hooks** return idle state by default:
-- `useSend()` — `{ send: vi.fn(), stage: "idle", isLoading: false }`. Its `result` type is `SendResult { txId, note }` — distinct from `TransactionResult { transactionId }` used by `useMint`/`useConsume`/`useSwap`/`useMultiSend`/`useTransaction`.
-- `useMint()`, `useConsume()`, `useSwap()`, `useTransaction()`, `useMultiSend()` — idle shape with `result: TransactionResult | null`.
-- `useCreateWallet()` — `{ createWallet: vi.fn(), isCreating: false }`.
-
-### Simulating transaction stages
-
-```tsx
-// Show "proving" stage
-vi.mocked(useSend).mockReturnValue({
- send: vi.fn(),
- result: null,
- isLoading: true,
- stage: "proving",
- error: null,
- reset: vi.fn(),
-});
-
-// Show completed transaction — useSend returns SendResult { txId, note }
-vi.mocked(useSend).mockReturnValue({
- send: vi.fn(),
- result: { txId: "0xabc123", note: null },
- isLoading: false,
- stage: "complete",
- error: null,
- reset: vi.fn(),
-});
-
-// Other mutation hooks return TransactionResult { transactionId }
-vi.mocked(useMint).mockReturnValue({
- mint: vi.fn(),
- result: { transactionId: "0xdef456" },
- isLoading: false,
- stage: "complete",
- error: null,
- reset: vi.fn(),
-});
-```
-
-## Fixtures
-
-Realistic test data in `src/__tests__/fixtures/`:
-
-```tsx
-import {
- WALLET_ID_1, // "0x0a00000000000001"
- WALLET_ID_2, // "0x0a00000000000002"
- FAUCET_ID, // "0x0a00000000000003"
- COUNTER_ID, // "0x0a00000000000004"
- MOCK_WALLET_HEADER, // { id, nonce, storageCommitment }
- MOCK_FAUCET_HEADER, // { id, nonce, storageCommitment }
- MOCK_ASSET_BALANCE, // { assetId, amount: 1000000000n, symbol: "TEST", decimals: 8 }
- MOCK_ACCOUNT, // { id, nonce, bech32id() }
- MOCK_TRANSACTION_RESULT, // { transactionId: "0x..." } — useMint / useConsume / useSwap / useMultiSend / useTransaction
- MOCK_SEND_RESULT, // { txId: "0x...", note: null } — useSend
- MOCK_NOTE_SUMMARY, // { id, assets, sender }
-} from "@/__tests__/fixtures";
-```
-
-Key characteristics:
-- Account IDs use hex format (`0x...`) — network-agnostic test fixtures
-- Amounts are `bigint` (e.g., `1000000000n` = 10.0 with 8 decimals)
-- Asset metadata uses TEST token with 8 decimals
-
-## Test Patterns (copy-adaptable)
-
-Reference tests in `src/__tests__/patterns/`:
-
-| Pattern | File | Tests |
-|---------|------|-------|
-| Provider/context setup | `provider-setup.test.tsx` | ready, loading, error states |
-| Query hook component | `query-hook.test.tsx` | data, loading, error, empty states |
-| Mutation hook component | `mutation-hook.test.tsx` | idle, stages, success, error, argument verification |
-
-### Minimum test coverage per component
-
-Every component test should cover:
-1. **Success state** — renders correctly with data
-2. **Loading state** — shows loading indicator
-3. **Error state** — shows error message, recovery action
-4. **User interactions** — buttons, forms trigger correct handler calls
-
-## Wallet connection state in tests
-
-The [frontend template](https://github.com/0xMiden/frontend-template)'s wallet button (in `src/components/AppContent.tsx`) drives off **`useMidenFiWallet()`** from `@miden-sdk/miden-wallet-adapter-react`, not the generic `useSigner()`. The button gates on `wallet.readyState` (from `@miden-sdk/miden-wallet-adapter-base`) so the UI can render an "Install MidenFi Wallet" state before the extension is detected, rather than falling through to the adapter's Chrome-Web-Store fallback. When testing wallet-connect UI, mock both modules and override per test.
-
-Setup at the top of the test file:
-
-```tsx
-vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
-vi.mock("@miden-sdk/miden-wallet-adapter-react", () => ({
- useMidenFiWallet: vi.fn(() => ({
- wallet: null,
- connected: false,
- connecting: false,
- connect: vi.fn(),
- disconnect: vi.fn(),
- })),
-}));
-vi.mock("@miden-sdk/miden-wallet-adapter-base", () => ({
- WalletReadyState: {
- Installed: "Installed",
- NotDetected: "NotDetected",
- Loadable: "Loadable",
- Unsupported: "Unsupported",
- },
-}));
-
-import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
-```
-
-Per-test overrides match the states the template renders:
-
-```tsx
-// extension not detected — shows disabled "Install MidenFi Wallet"
-vi.mocked(useMidenFiWallet).mockReturnValue({
- wallet: { adapter: {} as never, readyState: "NotDetected" } as never,
- connected: false,
- connecting: false,
- connect: vi.fn(),
- disconnect: vi.fn(),
-} as never);
-
-// installed + disconnected — shows "Connect Wallet"
-vi.mocked(useMidenFiWallet).mockReturnValue({
- wallet: { adapter: {} as never, readyState: "Installed" } as never,
- connected: false,
- connecting: false,
- connect: vi.fn(),
- disconnect: vi.fn(),
-} as never);
-
-// connected — shows "Disconnect Wallet"
-vi.mocked(useMidenFiWallet).mockReturnValue({
- wallet: { adapter: {} as never, readyState: "Installed" } as never,
- connected: true,
- connecting: false,
- connect: vi.fn(),
- disconnect: vi.fn(),
-} as never);
-```
-
-See `src/components/__tests__/AppContent.test.tsx` in the [frontend template](https://github.com/0xMiden/frontend-template) for the full pattern (including a `walletState()` helper that cuts per-test boilerplate).
-
-For app code that needs the selected signer account for client-side flows (transaction-building hooks, etc.), `useMiden()` exposes `signerAccountId` / `signerConnected` as lower-level provider state — mock those via the `@miden-sdk/react` mock factory.
-
-Vitest config externalizes `@miden-sdk/miden-wallet-adapter-react` to prevent broken transitive resolution.
-
-## Automated Verification Pipeline
-
-The [frontend template](https://github.com/0xMiden/frontend-template) ships a `.claude/settings.json` that wires Claude Code hooks to enforce quality automatically. All three checks live under a single `PostToolUse` matcher (`Edit|Write`) and fire on every `.ts`/`.tsx` edit in `src/` (the typecheck and affected-tests hooks early-exit otherwise); the template ships no `Stop` hook:
-
-1. **PostToolUse: typecheck** — `npx tsc -b --noEmit` on every `.ts`/`.tsx` edit in `src/`
-2. **PostToolUse: affected tests** — `npx vitest --changed --run` on every `.ts`/`.tsx` edit in `src/`
-3. **PostToolUse: full verification** — `npx vitest --run && npx tsc -b --noEmit && npx vite build` (same `Edit|Write` matcher), so the full suite + build run on each src edit rather than at task completion
-
-If any hook fails (exit code 2), the agent is blocked from proceeding until the issue is fixed. Copy the same hook layout into your own `.claude/settings.json` to get the same enforcement locally.
-
-## TDD Flow
-
-```
-1. Write test (describe expected behavior)
- ↓
-2. yarn test → RED (test fails)
- ↓
-3. Implement code
- ↓
-4. Auto hooks fire → typecheck + affected tests
- ↓
-5. yarn test → GREEN (all pass)
- ↓
-6. Refactor if needed
- ↓
-7. Task complete → full suite + build runs on each src edit (PostToolUse)
-```
-
-## Common Mistakes
-
-**Forgetting vi.clearAllMocks()**: Always call in `beforeEach` to prevent mock state leaking between tests.
-
-**Not mocking the SDK**: Components importing from `@miden-sdk/react` will fail without `vi.mock()` because the real SDK requires WASM initialization.
-
-**Using number instead of bigint for result/fixture amounts**: Result and fixture amounts are typed strictly as `bigint` (`AssetBalance.amount`, `NoteAsset.amount`, and `useAccount().getBalance()`), so mock them with bigint literals (`1000n`, not `1000`). Hook input options (`SendOptions.amount`, `MintOptions.amount`, `MultiSendRecipient.amount`, `CreateFaucetOptions.maxSupply`) accept `bigint | number`, but prefer bigint to avoid precision loss.
-
-**Testing implementation details**: Test what the user sees (text, buttons, states), not internal hook calls. Use `screen.getByRole`, `screen.getByText`, not internal component state.
diff --git a/skills/vite-wasm-setup/SKILL.md b/skills/vite-wasm-setup/SKILL.md
deleted file mode 100644
index c8d1d9d..0000000
--- a/skills/vite-wasm-setup/SKILL.md
+++ /dev/null
@@ -1,140 +0,0 @@
----
-name: vite-wasm-setup
-description: Guide to configuring Vite for Miden WASM applications. Covers the midenVitePlugin() setup, COOP/COEP headers, production deployment headers, TypeScript compatibility, and troubleshooting common Vite + WASM issues. Use when setting up a new Miden frontend, debugging build or runtime errors related to WASM or Vite configuration, or deploying to production.
----
-
-# Vite + WASM Configuration for Miden
-
-## Required `vite.config.ts`
-
-```typescript
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-import { midenVitePlugin } from "@miden-sdk/vite-plugin";
-
-export default defineConfig({
- plugins: [react(), midenVitePlugin()],
-});
-```
-
-`midenVitePlugin()` works with no options for the common case — the default `@miden-sdk/miden-sdk` / `@miden-sdk/react` imports ship **single-threaded (ST)** WASM that loads in any browser context, so the default client runs with no cross-origin isolation. The plugin's `crossOriginIsolation` option defaults to `false` for the same reason, and the v0.15.0 example wallet app calls `midenVitePlugin()` bare. Don't reach for `crossOriginIsolation: true` unless you have actually opted into the multi-threaded build (see below).
-
-Pass `crossOriginIsolation: true` **only** if you import the **multi-threaded (MT)** WASM variant — `@miden-sdk/miden-sdk/mt` (or `/mt/lazy`) and `@miden-sdk/react/mt` (or `/mt/lazy`). The MT build uses `wasm-bindgen-rayon` and `SharedArrayBuffer` / `WebAssembly.Memory({ shared: true })` for ~3–5x faster local proving, which the browser only constructs when the page is cross-origin-isolated (COOP `same-origin` + COEP `require-corp`). On the default ST imports those headers are unnecessary. The [frontend template](https://github.com/0xMiden/frontend-template)'s `vite.config.ts` is the source-of-truth reference for the current setup.
-
-If your app must host third-party iframes, OAuth popups, or other cross-origin resources that don't emit `require-corp`, stay on the default ST imports and leave `crossOriginIsolation: false` (the default) — you keep a fully working Miden client and only forgo MT-accelerated local proving on that route. Enabling `crossOriginIsolation: true` also breaks OAuth-popup flows (e.g. Para), because `same-origin` COOP nullifies `window.opener` in popups. If you genuinely need both MT proving and cross-origin resources, embed the latter via `credentialless` COEP as a workaround (see the Gotchas section below).
-
-## What midenVitePlugin() Handles
-
-`@miden-sdk/vite-plugin` abstracts Miden-specific Vite configuration. It does **not** register a `.wasm` module loader — Vite's built-in handling does the actual `.wasm` import. What the plugin sets up:
-
-- **WASM dedup / single copy** — `resolve.alias` (exact-match regex on the WASM package), `resolve.dedupe`, and `resolve.preserveSymlinks` force a single resolved copy of `@miden-sdk/miden-sdk` (avoids WASM class-identity issues across symlinked/monorepo setups)
-- **optimizeDeps.exclude** — Excludes `@miden-sdk/miden-sdk` from pre-bundling (pre-bundling corrupts the WASM binary)
-- **Top-level await** — Sets `build.target: "esnext"`, which enables the top-level `await` the WASM SDK initialization requires
-- **ES-module workers** — Sets `worker.format: "es"`, required for the WASM SDK's module workers
-- **COOP/COEP headers (opt-in, MT only)** — `crossOriginIsolation` defaults to `false`. When set to `true`, emits `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp` on **both** the Vite dev server and the Vite preview server (see Production Deployment Headers). Only needed to satisfy the cross-origin-isolation requirement of the MT WASM variant; the default ST build doesn't need these headers
-- **gRPC-web dev proxy** — Proxies `/rpc.Api` to `rpcProxyTarget` (default `https://rpc.testnet.miden.io`) during `vite` (serve) to bypass CORS in dev; set `rpcProxyTarget: false` to disable
-- **React context dedup** — Externalizes `@miden-sdk/react` during esbuild pre-bundling so signer-provider React contexts share one identity
-
-You don't need to install or configure `vite-plugin-wasm`, `vite-plugin-top-level-await`, or dexie aliases manually.
-
-## Required Dependencies
-
-Two packages move together as the core SDK pair: `@miden-sdk/miden-sdk` (the WASM client) and `@miden-sdk/react` (the React hooks). At v0.15.0 both are `0.15.0` and share a WASM ABI, so they must match. The **vite-plugin and the wallet adapters are versioned independently** and can trail the core SDK by a minor/patch — don't assume they're in lockstep. The [frontend template](https://github.com/0xMiden/frontend-template)'s `package.json` is the reference for the current pin set; re-run your app's full build + end-to-end suite whenever you bump.
-
-```json
-{
- "dependencies": {
- "@miden-sdk/react": "",
- "@miden-sdk/miden-sdk": "",
- "@miden-sdk/miden-wallet-adapter-react": ""
- },
- "devDependencies": {
- "@miden-sdk/vite-plugin": ""
- }
-}
-```
-
-Notes:
-- **`@miden-sdk/react` and `@miden-sdk/miden-sdk` must match** — they link against the same WASM ABI, so a mixed pair (e.g. one built against an older WASM ABI, one against the current) won't link. Upgrade them together.
-- **The `@miden-sdk/vite-plugin` does NOT track the core SDK version.** At v0.15.0 the plugin trails the core SDK by a minor and is NOT on the same version as `@miden-sdk/miden-sdk@0.15.0`; they only realign later in the 0.15 line. Always defer to your app's `package.json` (or the frontend template's) for the authoritative plugin pin — never assume `vite-plugin === miden-sdk`.
-- **The wallet adapters live in a separate repo.** `@miden-sdk/miden-wallet-adapter-react` (and its companion `@miden-sdk/miden-wallet-adapter-base`) are published from [`0xMiden/wallet-adapter`](https://github.com/0xMiden/wallet-adapter), not the web-sdk repo, and are versioned independently. Confirm the exact package names and versions against that repo (or your app's `package.json`); the `-react` adapter's `peerDependencies` pin `@miden-sdk/react` at `^..x`, so a patch-level gap from the core SDK is expected and fine.
-- **Always check your app's `package.json` (or the [frontend template](https://github.com/0xMiden/frontend-template)'s) for the authoritative versions** — this skill intentionally doesn't inline them because they shift across SDK releases.
-- When you bump, do a clean install with your project's package manager: delete `node_modules` and the lockfile it actually uses, then reinstall. The web-sdk uses pnpm (`rm -rf node_modules pnpm-lock.yaml && pnpm install`). For an app repo, use whatever package manager its lockfile implies — e.g. the frontend template's v0.15 branch ships a `yarn.lock` (`rm -rf node_modules && yarn install`), while another app may use `npm ci` or `pnpm install`. Vite's dep optimizer caches resolved SDK paths, and stale caches can surface as `ERR_BLOCKED_BY_RESPONSE` or spurious `Failed to fetch` errors on module workers.
-
-## Production Deployment Headers
-
-These headers apply **only if you ship the MT WASM variant** (`/mt` or `/mt/lazy`). The default ST build needs none of this — skip the whole section if you're on the default imports. If you are on MT, the COOP/COEP headers must be set on the production server: `midenVitePlugin({ crossOriginIsolation: true })` only emits them on the Vite dev server (`vite`) and the Vite preview server (`vite preview`) — it does not touch your real production host. Configure the headers separately on nginx/Vercel/Cloudflare/etc.
-
-### Nginx
-```nginx
-add_header Cross-Origin-Opener-Policy same-origin;
-add_header Cross-Origin-Embedder-Policy require-corp;
-```
-
-### Vercel (vercel.json)
-```json
-{
- "headers": [
- {
- "source": "/(.*)",
- "headers": [
- { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
- { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
- ]
- }
- ]
-}
-```
-
-### Cloudflare Pages (_headers)
-```
-/*
- Cross-Origin-Opener-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
-```
-
-### WASM MIME Type
-Ensure your server serves `.wasm` files with `application/wasm` MIME type.
-
-## COOP/COEP Gotchas
-
-These gotchas only apply once you've enabled cross-origin isolation for the MT build — the default ST build sets no such headers and is unaffected. When COOP `same-origin` + COEP `require-corp` are in force, they break:
-- **Third-party iframes** (YouTube embeds, Twitter embeds, analytics)
-- **External scripts** without CORS headers
-- **OAuth popups** from different origins
-
-Workaround: Use `credentialless` for COEP if you need cross-origin resources:
-```
-Cross-Origin-Embedder-Policy: credentialless
-```
-
-Note: `credentialless` provides weaker isolation but allows most cross-origin resources.
-
-## TypeScript Compatibility
-
-Standard Vite-compatible tsconfig settings work with Miden. The only actual constraint is ES2020+ for `bigint` support:
-
-```json
-{
- "compilerOptions": {
- "target": "ES2022",
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "moduleResolution": "bundler"
- }
-}
-```
-
-`module: "ESNext"` and `moduleResolution: "bundler"` are standard Vite defaults, not Miden-specific requirements. If you're using the Vite-generated tsconfig, no changes are needed beyond ensuring `target` is ES2020+.
-
-## Troubleshooting
-
-| Issue | Cause | Fix |
-|-------|-------|-----|
-| "SharedArrayBuffer is not defined" (MT build only) | Importing `/mt` or `/mt/lazy` on a page that isn't cross-origin-isolated | Set `midenVitePlugin({ crossOriginIsolation: true })` and add the COOP/COEP headers on your production host; or switch back to the default ST imports, which don't need them |
-| WASM module not found | SDK not configured correctly | Ensure `midenVitePlugin()` is in plugins array |
-| "Top-level await not supported" | Missing plugin setup | Ensure `midenVitePlugin()` is in plugins array |
-| WASM init hangs | COEP blocking WASM fetch | Check network tab for blocked requests; verify COOP/COEP headers are present |
-| Build succeeds but WASM fails at runtime | Wrong MIME type | Serve .wasm as application/wasm |
-| "recursive use of an object" | Concurrent WASM access | Use runExclusive() from useMiden() |
-| Double initialization in dev | React StrictMode | Use MidenProvider (handles this internally) |
diff --git a/skills/wasm-bridge/SKILL.md b/skills/wasm-bridge/SKILL.md
deleted file mode 100644
index 860059d..0000000
--- a/skills/wasm-bridge/SKILL.md
+++ /dev/null
@@ -1,441 +0,0 @@
----
-name: wasm-bridge
-description: Enforce conventions for the Rust<->JavaScript WASM boundary in the web-sdk repo (crate miden-client-web at crates/web-client, split out of miden-client). Use when exposing Rust methods to JS via the #[js_export] proc-macro, creating newtype wrappers, handling errors across the boundary with JsErr, bridging JS Promises to Rust Futures, or layering the public MidenClient resource API on top of the WASM-bound WebClient.
----
-
-# WASM Bridge Patterns (web-client / miden-client-web)
-
-At v0.15 the web client lives in the dedicated **web-sdk** repo
-(`github.com/0xMiden/web-sdk`), split out of `miden-client`. The Rust<->JS
-boundary crate is `crates/web-client` (cargo package `miden-client-web`).
-Companion workspace crates: `crates/js-export-macro` (the `#[js_export]`
-proc-macro) and `crates/idxdb-store` (the IndexedDB store).
-
-The crate dual-targets two binding technologies from one Rust source:
-- **browser** (the `browser` feature) via `wasm_bindgen`, error type `JsValue`
-- **Node.js** (the `nodejs` feature) via `napi` / `napi-derive`, error type
- `napi::Error`
-
-A platform abstraction layer in `crates/web-client/src/platform.rs` provides
-type aliases and helpers so most code is written once. Key aliases:
-
-- `JsErr` — the platform error type (`wasm_bindgen::JsValue` on browser,
- `napi::Error` on nodejs). `from_str_err(msg: &str) -> JsErr` builds one from a
- string.
-- `JsU64` — `u64` on browser, `napi::bindgen_prelude::BigInt` on nodejs; both
- surface as a JS `BigInt`. Convert with `js_u64_to_u64` / `u64_to_js_u64`.
-- `JsBytes` — `js_sys::Uint8Array` on browser, `napi::bindgen_prelude::Buffer`
- on nodejs. Convert with `bytes_to_js` / `js_to_bytes`.
-- `AsyncCell` — interior mutability: `RefCell` on browser, `tokio::sync::Mutex`
- on nodejs; `.lock().await` yields a `DerefMut` guard.
-
-## Exposing Rust Methods to JavaScript
-
-### Method Annotation — `#[js_export]`
-
-The public API is exposed with the custom `#[js_export]` proc-macro from the
-`js-export-macro` crate, **not** raw `#[wasm_bindgen]`. `#[js_export]` generates
-the dual `wasm_bindgen` (browser) and `napi` (Node.js) annotations from one
-attribute, forwarding `constructor` / `js_name` / `getter`. When a signature
-contains `JsU64`, the macro splits the impl per platform, replacing `JsU64` with
-`u64` (browser) or `BigInt` (nodejs) — so `JsU64` is resolved by the macro and
-does not need to be imported in the annotated module. Raw `#[wasm_bindgen]` is
-reserved for browser-only members (e.g. synchronous getters that cannot be async).
-
-Apply `#[js_export]` to the struct/enum/impl block, and `#[js_export(js_name =
-"camelCase")]` to each method to map snake_case Rust to camelCase JS:
-
-```rust
-use js_export_macro::js_export;
-
-use crate::models::account_header::AccountHeader;
-use crate::platform::{JsErr, from_str_err};
-use crate::{WebClient, js_error_with_context};
-
-#[js_export]
-impl WebClient {
- #[js_export(js_name = "getAccounts")]
- pub async fn get_accounts(&self) -> Result, JsErr> {
- let mut guard = self.get_mut_inner().await;
- let client = guard
- .as_mut()
- .ok_or_else(|| from_str_err("Client not initialized"))?;
-
- let result = client
- .get_account_headers()
- .await
- .map_err(|err| js_error_with_context(err, "failed to get accounts"))?;
-
- Ok(result.into_iter().map(|(header, _)| header.into()).collect())
- }
-}
-```
-
-Rules:
-- Annotate with `#[js_export]` (struct/impl) and `#[js_export(js_name = ...)]`
- (methods). Use `#[js_export(constructor)]` for constructors,
- `#[js_export(getter)]` for getters. Use raw `#[wasm_bindgen]` only for
- browser-only items.
-- Methods take `&self` (the inner client is behind an `AsyncCell`/lock, so no
- `&mut self`). Acquire the client with `let mut guard =
- self.get_mut_inner().await;` then `let client = guard.as_mut().ok_or_else(||
- from_str_err("Client not initialized"))?;`. `get_mut_inner` returns a
- `DerefMut` guard over `Option>`.
-- Return `Result` — never `Result` directly, and never
- panic across the boundary.
-- Use `.map_err(|err| js_error_with_context(err, "context"))` for all fallible
- client calls.
-- Convert return types via `.into()` (implement `From` on wrapper types).
-
-## Error Handling Across the Boundary
-
-### js_error_with_context
-
-Use the `js_error_with_context` helper (in `crates/web-client/src/lib.rs`) to
-chain error sources and attach hints. It returns `JsErr` and splits per
-platform; the browser branch additionally attaches a stable machine-readable
-`code`:
-
-```rust
-pub(crate) fn js_error_with_context(err: T, context: &str) -> JsErr
-where
- T: Error + 'static,
-{
- let error_message = build_error_chain(context, &err);
- let help = hint_from_error(&err);
-
- #[cfg(feature = "browser")]
- {
- let js_error: JsValue = JsError::new(&error_message).into();
- if let Some(help) = help {
- let _ = Reflect::set(&js_error, &JsValue::from_str("help"), &JsValue::from_str(&help));
- }
- // Stable, machine-readable code for the ClientError variants JS callers
- // branch on, so they don't depend on (changeable) message text.
- if let Some(code) = code_from_error(&err) {
- let _ = Reflect::set(&js_error, &JsValue::from_str("code"), &JsValue::from_str(code));
- }
- js_error
- }
-
- #[cfg(feature = "nodejs")]
- {
- let message = match help {
- Some(help) => format!("{error_message} [help: {help}]"),
- None => error_message,
- };
- napi::Error::from_reason(message)
- }
-}
-```
-
-This:
-1. Chains all error sources into one message via `build_error_chain(context,
- &err)` (walks `err.source()`, writing `context: err1: err2: ...`).
-2. Extracts an `ErrorHint` from `ClientError` via `hint_from_error` if available.
-3. Browser path: attaches `help` (the hint) and `code` (from `code_from_error`,
- which maps the few `ClientError` variants JS callers branch on, e.g.
- `ACCOUNT_NOT_FOUND_ON_CHAIN`, `ACCOUNT_ALREADY_TRACKED`) as properties on the
- JS `Error` via `Reflect::set`.
-4. Node.js path: returns `napi::Error::from_reason(...)` with the help inlined
- into the message.
-
-### Error Pattern in Every Method
-
-```rust
-client
- .some_operation()
- .await
- .map_err(|err| js_error_with_context(err, "failed to "))?;
-```
-
-The context string should be lowercase and describe the failed operation. For
-the not-initialized guard, build the error with `from_str_err("Client not
-initialized")` (the platform helper), not `JsValue::from_str(...)`.
-
-## Newtype Wrappers
-
-### Pattern
-
-Wrap native Miden types in thin newtypes for JS exposure, annotated with
-`#[js_export]`. Fallible construction returns `Result`:
-
-```rust
-use js_export_macro::js_export;
-use miden_client::{Felt as NativeFelt, Word as NativeWord};
-use crate::platform::{JsBytes, JsErr, from_str_err, js_u64_to_u64, u64_to_js_u64};
-
-#[derive(Clone)]
-#[js_export]
-pub struct Word(NativeWord);
-
-#[js_export]
-impl Word {
- #[js_export(constructor)]
- pub fn new(u64_vec: Vec) -> Result {
- if u64_vec.len() != 4 {
- return Err(from_str_err(&format!(
- "Word requires exactly 4 elements, got {}",
- u64_vec.len()
- )));
- }
- let fixed_array_u64: [u64; 4] = u64_vec
- .into_iter()
- .map(js_u64_to_u64)
- .collect::>()
- .try_into()
- .expect("length checked above");
- let native_felt_vec: [NativeFelt; 4] = fixed_array_u64
- .iter()
- .map(|&v| NativeFelt::new(v)) // fallible on the 0.15 surface
- .collect::, _>>()
- .map_err(|err| from_str_err(&format!("invalid field element: {err}")))?
- .try_into()
- .expect("length checked above");
- Ok(Word(native_felt_vec.into()))
- }
-
- #[js_export(js_name = "fromHex")]
- pub fn from_hex(hex: String) -> Result {
- let native_word = NativeWord::try_from(hex.as_str())
- .map_err(|err| from_str_err(&format!("Error instantiating Word from hex: {err}")))?;
- Ok(Word(native_word))
- }
-}
-```
-
-Notes:
-- `JsU64` (BigInt-aware) is used for numeric inputs, not `u64`, so full 64-bit
- precision survives the JS `Number`/`BigInt` boundary. The `#[js_export]` macro
- rewrites `JsU64` per platform, so it is referenced unqualified and is not
- imported alongside the `js_u64_to_u64` / `u64_to_js_u64` converters.
-- Constructors that can fail (length checks, fallible `Felt::new`) return
- `Result<_, JsErr>`; do not paper over failures with `.unwrap()`.
-- `from_hex` takes `String` (not `&str`) and returns `Result`.
-
-### Required Conversions and Accessors
-
-Implement the `From` conversions, and put the internal `as_native` accessor in a
-**plain** `impl` block (not under `#[js_export]`, since it is `pub(crate)`):
-
-```rust
-impl Word {
- pub(crate) fn as_native(&self) -> &NativeWord {
- &self.0
- }
-}
-
-// Native -> Wrapper (by value and by ref)
-impl From for Word {
- fn from(native_word: NativeWord) -> Self { Word(native_word) }
-}
-impl From<&NativeWord> for Word {
- fn from(native_word: &NativeWord) -> Self { Word(*native_word) }
-}
-
-// Wrapper -> Native (by value and by ref)
-impl From for NativeWord {
- fn from(word: Word) -> Self { word.0 }
-}
-impl From<&Word> for NativeWord {
- fn from(word: &Word) -> Self { word.0 }
-}
-```
-
-For wrapper newtypes that must be accepted as by-value or `Vec` parameters on
-the Node.js side, also invoke `impl_napi_from_value!(Word);` (defined in
-`crates/web-client/src/miden_array.rs`; a no-op under the `browser` feature). It
-bridges napi-rs v3's missing `FromNapiValue` for `#[napi]` class types.
-
-### Factory Methods
-
-Provide `fromHex()`-style constructors that return `Result` for
-user-facing types.
-
-## Data Transfer Objects
-
-For complex data that crosses the WASM boundary, use a dual-platform
-`getter_with_clone` / `napi(object)` struct (gated with `cfg_attr`), and map
-field names with browser-side `js_name`:
-
-```rust
-#[cfg_attr(feature = "browser", wasm_bindgen(getter_with_clone, inspectable))]
-#[cfg_attr(feature = "nodejs", napi(object))]
-#[derive(Clone)]
-pub struct StorageMapEntry {
- #[cfg_attr(feature = "browser", wasm_bindgen(js_name = "root"))]
- pub root: String,
- #[cfg_attr(feature = "browser", wasm_bindgen(js_name = "key"))]
- pub key: String,
- #[cfg_attr(feature = "browser", wasm_bindgen(js_name = "value"))]
- pub value: String,
-}
-```
-
-Rules:
-- Use the dual-platform `#[cfg_attr(feature = "browser", wasm_bindgen(...))]` +
- `#[cfg_attr(feature = "nodejs", napi(object))]` form — never a bare
- `#[wasm_bindgen(getter_with_clone)]`.
-- `getter_with_clone` auto-generates JS getters; `inspectable` improves console
- inspection. `inspectable` can also stand alone (without `getter_with_clone`)
- on an opaque wrapper class, again via the dual form `#[cfg_attr(feature =
- "browser", wasm_bindgen(inspectable))]` + `#[cfg_attr(feature = "nodejs",
- napi)]` (note: bare `napi`, not `napi(object)`, for a class that wraps a
- native handle rather than a plain-data object).
-- Field names: snake_case in Rust, camelCase via browser-side `js_name`.
-- Serialize complex values to hex strings or `JsBytes`/`Vec` where needed.
-
-## Promise Handling (idxdb-store pattern)
-
-When calling JS functions from Rust that return Promises (the IndexedDB store,
-in `crates/idxdb-store`), use these helpers (`crates/idxdb-store/src/promise.rs`):
-
-```rust
-/// Awaits a JavaScript Promise and returns the raw JsValue.
-pub(crate) async fn await_js_value(promise: Promise, ctx: &str) -> Result {
- JsFuture::from(promise)
- .await
- .map_err(|js_error| StoreError::DatabaseError(format!("{ctx}: {js_error:?}")))
-}
-
-/// Awaits a JavaScript Promise and deserializes into T.
-pub(crate) async fn await_js(promise: Promise, ctx: &str) -> Result
-where
- T: DeserializeOwned,
-{
- let js_value = await_js_value(promise, ctx).await?;
- from_value(js_value)
- .map_err(|err| StoreError::DatabaseError(format!("failed to deserialize ({ctx}): {err:?}")))
-}
-
-/// Awaits a JavaScript Promise and discards the result.
-pub(crate) async fn await_ok(promise: Promise, ctx: &str) -> Result<(), StoreError> {
- let _ = await_js_value(promise, ctx).await?;
- Ok(())
-}
-```
-
-Rules:
-- Always provide a context string describing what the await is for.
-- Use `await_js::()` when you need to deserialize the result.
-- Use `await_ok()` when you only care about success/failure.
-- Use `serde_wasm_bindgen::from_value()` for deserialization, not `serde_json`.
- (At v0.15 `Promise` is imported via `wasm_bindgen_futures::js_sys::Promise`.)
-
-## Importing JS Functions from Rust
-
-Declare external JS functions with `#[wasm_bindgen(module = "...")]` (browser /
-idxdb-store side):
-
-```rust
-#[wasm_bindgen(module = "/src/js/utils.js")]
-extern "C" {
- #[wasm_bindgen(js_name = logWebStoreError)]
- fn log_web_store_error(error: JsValue, error_context: alloc::string::String);
-}
-
-#[wasm_bindgen(module = "/src/js/schema.js")]
-extern "C" {
- /// Opens the database and registers it in the JS registry.
- #[wasm_bindgen(js_name = openDatabase)]
- fn open_database(network: &str, client_version: &str) -> js_sys::Promise;
-}
-```
-
-Rules:
-- Module path is relative to the crate root.
-- Function names are snake_case in Rust, mapped via `js_name`.
-- Return `js_sys::Promise` for async operations.
-- Pass simple types across the boundary: `&str`, `JsValue`, `Vec`, `u32`.
-
-## JS Wrapper Layer
-
-The web-client crate ships **two** JS layers under `crates/web-client/js/`:
-
-1. **`WebClient`** (`js/index.js`) — the WASM-bound class re-exported as
- `WasmWebClient` (`export { WebClient as WasmWebClient, MockWebClient as
- MockWasmWebClient }`). It wraps the `WebClient` Rust struct and adds JS-side
- concerns:
- - `_serializeWasmCall` queue that linearizes WASM calls (the inner client is
- behind a lock, so the JS side must not interleave async calls).
- - `syncState()` is wrapped in the exported `withSyncLock(dbId, methodId, fn)`
- helper (`js/syncLock.js`, Web Locks via `navigator.locks`) to coalesce
- concurrent syncs and serialize them across tabs:
- `return await withSyncLock(dbId, methodId, async () =>
- this._serializeWasmCall(...))`.
- - method-classification sets (`SYNC_METHODS`, `READ_METHODS`,
- `WRITE_METHODS`) consumed by the proxy and enforced by
- `scripts/check-method-classification.js`. (`SYNC_METHODS` is a historical
- misnomer — it groups methods safe to bind raw.)
-2. **`MidenClient`** (`js/client.js`) — the public, resource-based wrapper that
- owns a `WebClient` instance and exposes typed sub-objects: `client.accounts`,
- `client.transactions`, `client.notes`, `client.tags`, `client.settings`,
- `client.compile` (a `CompilerResource`, hence the property is `compile`
- though the file is `compiler.js`), and `client.keystore`. Each resource lives
- under `js/resources/.js`.
-
-`index.js` injects the WASM constructor and the `getWasm` initializer into
-`MidenClient` via static fields to break the import cycle:
-
-```javascript
-MidenClient._WasmWebClient = WebClient;
-MidenClient._MockWasmWebClient = MockWebClient;
-MidenClient._getWasmOrThrow = getWasmOrThrow;
-```
-
-There is **no** `safe-arrays.js` module. The wasm-bindgen array wrappers
-(`NoteArray`, `OutputNoteArray`, `AccountArray`, `ForeignAccountArray`, ...) are
-generated by the `declare_js_miden_arrays!` macro (defined in
-`crates/web-client/src/miden_array.rs`, invoked in
-`crates/web-client/src/models/mod.rs`), and their constructor **consumes** its
-elements. To keep an element usable afterwards, construct the array empty and
-`push` by reference instead of passing elements to the constructor:
-
-```javascript
-// NoteArray constructor consumes its elements; use push(¬e) to keep
-// `note` valid so it can be returned to the caller.
-const ownOutputs = new wasm.NoteArray();
-ownOutputs.push(note);
-```
-
-### Adding a method
-
-When extending the SDK, choose the layer based on whether the work is
-**Rust-side** or **glue/shape**:
-
-- **Rust-side logic** (new RPC call, new transaction request type, storage
- access): expose a method on the WASM `WebClient` impl with `#[js_export(js_name
- = "camelCase")]`, then surface it from the matching resource in
- `js/resources/`. Update the method-classification sets in `index.js` so the
- linter (`scripts/check-method-classification.js`) accepts it.
-- **JS-side ergonomics** (option-bag normalization, account-ref resolution, type
- coercion): keep the work in the resource module and call the existing WASM
- method.
-
-Resource methods follow this shape:
-
-```javascript
-// crates/web-client/js/resources/accounts.js
-async get(ref) {
- this.#client.assertNotTerminated();
- const wasm = await this.#getWasm();
- const id = resolveAccountRef(ref, wasm); // accepts string | AccountId | Account | AccountHeader
- const account = await this.#inner.getAccount(id);
- return account ?? null;
-}
-```
-
-Rules:
-
-- Always call `this.#client.assertNotTerminated()` at entry — late callbacks on
- a torn-down client otherwise panic with "null pointer passed to rust".
-- Resolve account/note/storage refs through the helpers in
- `crates/web-client/js/utils.js` (e.g. `resolveAccountRef`,
- `resolveStorageMode`), imported from a resource as `../utils.js`, so callers
- can pass any natural form (hex, bech32 address, WASM type). (There is no
- `utils.js` inside `js/resources/` — that directory holds only the seven
- resource files: accounts, compiler, keystore, notes, settings, tags,
- transactions.)
-- Return WASM-owned objects (e.g. `Account`, `AccountHeader`) directly when
- callers will use them again — wrapping them in plain JS DTOs forces another
- WASM round-trip and breaks identity for code that compares by reference.
diff --git a/skills/web-client-usage/SKILL.md b/skills/web-client-usage/SKILL.md
deleted file mode 100644
index 426c8a4..0000000
--- a/skills/web-client-usage/SKILL.md
+++ /dev/null
@@ -1,486 +0,0 @@
----
-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.
----
-
-# Web SDK Usage Patterns
-
-This skill targets the `@miden-sdk/miden-sdk` npm package published from
-[`0xMiden/web-sdk`](https://github.com/0xMiden/web-sdk) (the JS web client; in
-0.15 it builds on the `miden-client` Rust crate). 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 public surface. The underlying WASM-bound class is
-exported as `WasmWebClient` (an alias for `WebClient`) for low-level
-operations the resource API does not yet wrap — reach for it via the wrapped
-`#inner` only when you must.
-
-## Client Initialization
-
-### Convenience constructors (recommended)
-
-```typescript
-import { MidenClient } from "@miden-sdk/miden-sdk";
-
-// Testnet — autoSync on, testnet RPC + prover + note transport
-const client = await MidenClient.createTestnet();
-
-// Devnet equivalent
-const client = await MidenClient.createDevnet();
-```
-
-Both accept the same `ClientOptions` for overrides:
-
-```typescript
-const client = await MidenClient.createTestnet({
- storeName: "my-app-tests", // isolates the IndexedDB store
- proverUrl: "local", // prove locally instead of remote
- autoSync: false, // disable initial sync
-});
-```
-
-### Generic constructor
-
-```typescript
-const client = await MidenClient.create({
- rpcUrl: "https://rpc.testnet.miden.io", // string URL or "testnet"/"devnet"/"localhost"
- noteTransportUrl: "https://transport.miden.io",
- storeName: "my-store",
- seed: new Uint8Array(32), // optional — deterministic key generation
- proverUrl: "testnet", // optional — sets a default prover
- autoSync: true, // optional — call sync() after init
- keystore: { // optional — external HSM/keystore
- getKey: async (pubKey) => { /* return secretKey or null */ },
- insertKey: async (pubKey, secretKey) => { /* persist */ },
- sign: async (pubKey, signingInputs) => { /* return signature */ },
- },
-});
-```
-
-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:
-
-```typescript
-await MidenClient.ready();
-const client = await MidenClient.createTestnet();
-```
-
-### Termination
-
-```typescript
-client.terminate(); // free WASM resources, close the store handle
-```
-
-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.**
-
-```typescript
-const summary = await client.sync(); // returns SyncSummary
-const height = await client.getSyncHeight(); // current local block number
-```
-
-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`
-
-```typescript
-const id = AccountId.fromHex("0xabc123..."); // throws on invalid hex
-const id = Address.fromBech32("mtst1abc...").accountId();
-const hex = id.toString(); // "0x..."
-```
-
-Pass `AccountId` (or any account ref the resource accepts: a hex/bech32
-`string`, `Account`, `AccountHeader`, or `AccountId`) to resource methods —
-never raw strings to methods that ask for `AccountId` directly. Note that an
-`Address` object is **not** an account ref: `AccountRef = string | Account |
-AccountHeader | AccountId`, and the resolver only special-cases objects with an
-`.id()` method (`Address` exposes `accountId()`, not `id()`), so call
-`address.accountId()` first.
-
-`AccountId.fromHex` throws on malformed input; wrap in `try/catch` when
-accepting user input.
-
-### Amounts — Always `BigInt`
-
-```typescript
-BigInt(1000)
-1000n // numeric literal
-BigInt("1000")
-```
-
-Amount fields accept `number | bigint` (`SendOptions`/`MintOptions.amount`,
-`FaucetOptions.maxSupply`) and are coerced internally with `BigInt(...)`, so
-an integer `number` works and does **not** throw. The hazard is pre-conversion
-precision loss: a numeric literal above `Number.MAX_SAFE_INTEGER` (2^53) loses
-precision before it ever reaches `BigInt()`. Use `bigint` for any value that
-might exceed 2^53.
-
-### Visibility & Account Types
-
-```typescript
-import { NoteVisibility, AccountType, AuthScheme, StorageMode } from "@miden-sdk/miden-sdk";
-
-NoteVisibility.Public // "public"
-NoteVisibility.Private // "private"
-
-// AccountType is a faucet-kind selector with ONLY two members:
-AccountType.FungibleFaucet // 0
-AccountType.NonFungibleFaucet // 1
-
-AuthScheme.Falcon // default — Falcon-512 over Poseidon2
-AuthScheme.ECDSA // EcdsaK256Keccak
-
-StorageMode.Public
-StorageMode.Private
-```
-
-Use `NoteVisibility` strings with the high-level resource APIs — `NoteType` is a
-separate enum exported for the low-level WASM APIs and is easy to confuse with
-`NoteVisibility`, so do not pass it where a `NoteVisibility` is expected. Use
-`AuthScheme.Falcon` for the Poseidon2-based Falcon-512 scheme.
-
-`AccountType` exposes **only** `FungibleFaucet`/`NonFungibleFaucet`. There is no
-`MutableWallet`/`ImmutableWallet`/`MutableContract`/`ImmutableContract` member —
-those evaluate to `undefined`. Wallets and contracts are not chosen via
-`AccountType`: a wallet is the default (omit `type`), and a contract is any
-`accounts.create()` call that passes `components` (or `type:
-"MutableContract"`/`"ImmutableContract"` as strings). See "Account Creation".
-
-`StorageMode` has only `Public`/`Private`. There is no `StorageMode.Network`
-(accessing it yields `undefined`, which silently resolves to private).
-
-## Account Creation
-
-```typescript
-// Wallet — the default when no `type` is given (private, Falcon)
-const wallet = await client.accounts.create();
-
-// Wallet with explicit options — omit `type` (there is no
-// AccountType.*Wallet member; passing one would be undefined → default wallet)
-const wallet = await client.accounts.create({
- storage: "private",
- auth: AuthScheme.Falcon,
-});
-
-// Faucet — selected via AccountType.FungibleFaucet / NonFungibleFaucet
-const faucet = await client.accounts.create({
- type: AccountType.FungibleFaucet,
- storage: "public",
- symbol: "DAG",
- decimals: 8,
- maxSupply: 10_000_000n,
-});
-
-// Custom contract — selected by passing `components` (NOT by an AccountType
-// member). Requires seed and an AuthSecretKey.
-const component = await client.compile.component({ code: contractMasm, slots: [] });
-const contract = await client.accounts.create({
- seed: new Uint8Array(32),
- auth: secretKey, // AuthSecretKey, not the AuthScheme enum
- components: [component], // presence of `components` routes to a contract
-});
-```
-
-A contract is whatever `accounts.create()` call carries `components` — the
-string forms `type: "MutableContract"` / `"ImmutableContract"` also route to a
-contract, but the canonical selector is `components`. There is **no**
-`AccountType.MutableContract`; `type: AccountType.MutableContract` is
-`undefined` and, without `components`, would silently create a wallet.
-
-## Transactions
-
-The transactions API is option-bag-based and accepts any account ref
-(`Account`, `AccountHeader`, hex string, `AccountId`).
-
-### Send
-
-```typescript
-const { txId } = await client.transactions.send({
- account: wallet, // sender
- to: "0xrecipient...", // any account ref
- token: faucet, // faucet account ref — identifies the asset
- amount: 100n,
- type: NoteVisibility.Public, // optional, but defaults to "public" — see note below
- reclaimAfter: 100, // optional — sender can reclaim after this block
- timelockUntil: 50, // optional — recipient can consume after this block
- waitForConfirmation: true,
- timeout: 30_000,
-});
-```
-
-**`type` defaults to PUBLIC, not private** — for both `send` and `mint`, the
-note-type resolver treats an omitted/`undefined` `type` as
-`NoteVisibility.Public`. Omitting `type` therefore creates a **public** note (a
-privacy hazard). Always pass `type: NoteVisibility.Private` explicitly when a
-private note is required.
-
-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`.
-
-```typescript
-const { txId, note } = await client.transactions.send({
- account: wallet,
- to: "mtst1...", // account ref: hex/bech32 string, Account,
- // AccountHeader, or AccountId (not an Address)
- token: faucet,
- amount: 100n,
- type: NoteVisibility.Private,
- returnNote: true,
-});
-
-// Stream the note via the note-transport service.
-// `to` accepts a bech32 string, a 0x-hex string, an Account, or an AccountId
-// (resolved via resolveAddress). It does NOT accept a pre-parsed Address
-// object — that falls through to Address.fromAccountId(addr) and throws.
-await client.notes.sendPrivate({ note, to: "mtst1..." });
-```
-
-### Mint
-
-```typescript
-const { txId } = await client.transactions.mint({
- account: faucet, // faucet executes the mint
- to: targetAccountId, // recipient
- amount: 1000n,
- type: NoteVisibility.Public,
- waitForConfirmation: true,
-});
-```
-
-The transaction executes on the **faucet** — a frequent bug is passing the
-recipient as `account`.
-
-### Consume
-
-```typescript
-// Specific notes
-await client.transactions.consume({
- account: wallet,
- notes: [noteId1, noteRecord, "0xnote..."], // any of: hex, NoteId, InputNoteRecord, Note
- waitForConfirmation: true,
-});
-
-// Drain everything consumable for the account
-const { txId, consumed, remaining } = await client.transactions.consumeAll({
- account: wallet,
- maxNotes: 50, // optional cap
-});
-```
-
-### Swap
-
-```typescript
-await client.transactions.swap({
- account: wallet,
- offer: { token: tokenA, amount: 100n }, // field is `offer`, not `offered`
- request: { token: tokenB, amount: 50n }, // field is `request`, not `requested`
- type: NoteVisibility.Public, // swap-note visibility
- paybackType: NoteVisibility.Private, // payback-note visibility
-});
-```
-
-### Execute (custom scripts)
-
-```typescript
-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, // public — auto-fetched via RPC
- { 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" | "pswapCreate" | "pswapConsume" | "pswapCancel" | "custom", ... })`
-runs the same kernel as the real call but without proving or submitting,
-returning a summary suitable for UI confirmation screens. The `pswap*`
-operations correspond to the `transactions.pswapCreate` / `pswapConsume` /
-`pswapCancel` partial-swap methods.
-
-## Notes
-
-```typescript
-await client.notes.list(); // all input notes
-await client.notes.list({ status: "committed" }); // filter
-await client.notes.get(noteId); // single record
-await client.notes.listSent(); // output notes
-await client.notes.listAvailable({ account: wallet });// consumable for an account
-
-// Import/export
-await client.notes.import(noteFile);
-const file = await client.notes.export(noteId);
-
-// Private-note transport
-await client.notes.fetchPrivate(); // pulls anything addressed to tracked accounts
-await client.notes.sendPrivate({ note, to: "mtst1..." }); // `to`: bech32 string, 0x-hex string, Account, or AccountId (not a pre-parsed Address); delivers via the transport service
-```
-
-## Accounts (querying)
-
-```typescript
-await client.accounts.list(); // tracked accounts
-await client.accounts.get(ref); // single (returns null if not tracked)
-await client.accounts.getOrImport(ref); // tries get(), falls back to import()
-await client.accounts.getDetails(ref); // { account, vault, storage, code, keys }
-await client.accounts.insert({ account, overwrite }); // start tracking an existing account
-await client.accounts.getBalance(account, token); // single-asset balance, returns bigint
-```
-
-`getDetails(ref)` returns `{ account, vault, storage, code, keys }` — the full
-`Account`, its `AssetVault`, `AccountStorage`, `AccountCode | null`, and the key
-commitments (`Word[]`); there is no `status` field.
-
-For a single asset balance without loading the full vault, prefer
-`client.accounts.getBalance(account, token)` (returns `bigint`). It wraps the
-underlying WASM client's `accountReader(id)` lazy reader, which you can
-drop into directly for finer-grained reads.
-
-## Keystore
-
-```typescript
-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
-
-```typescript
-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.
-
-## Common Workflows
-
-### Mint and consume (fund a fresh wallet)
-
-```typescript
-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
-
-```typescript
-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
-
-1. **Forgetting to sync.** Notes won't appear, balances will be stale, foreign
- accounts will be at the wrong block.
-2. **`number` literals above 2^53 for amounts.** Amount fields accept
- `number | bigint` and coerce via `BigInt()` (no `TypeError`), but a numeric
- literal above `Number.MAX_SAFE_INTEGER` loses precision *before* coercion.
- Use `bigint` for large amounts.
-3. **Omitting `type` and expecting a private note.** `send`/`mint` default
- `type` to **public** — pass `NoteVisibility.Private` explicitly for privacy.
-4. **Passing a low-level `AccountId`-only WASM method a raw string** — resource
- methods accept hex/bech32 strings, but pre-parse with `AccountId.fromHex()`
- (and catch its throw) when calling APIs that demand an `AccountId` directly.
-5. **Consuming notes before they're committed** — sync first, check status.
-6. **Submitting `mint` with the recipient as `account`** — mint executes on
- the faucet account, not the target.
-7. **Private notes without transport** — must call `notes.sendPrivate()` (or
- pass `returnNote: true` to `transactions.send` and deliver out-of-band).
-8. **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.
-9. **Calling `accountReader(...)` in parallel with a write** — the readers
- share the WASM client. Wrap concurrent flows with `client.waitForIdle()`
- or rely on the React SDK's `runExclusive`.