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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.16.0-alpha.2 (TBD)

### Changes

* [BREAKING][web] Removed `notes.fetchPrivate({ mode: "all" })` (`WasmWebClient.fetchAllPrivateNotes`). `fetchPrivate()` now takes no arguments and always fetches incrementally from the stored pagination cursor. The full re-scan is no longer needed: historical notes for a newly tracked tag sit below the shared cursor and are now backfilled automatically during `sync()`, one tag at a time, so callers that previously reached for `mode: "all"` after adding a tag should just sync. Callers passing the option get a type error; the argument is otherwise ignored at runtime.
* [CHANGE][web] `miden-client` and `miden-client-sqlite-store` are pinned to rust-sdk `c39d2f0`, 17 commits past the `0.16.0-alpha.1` release, which adds the `debug-output` feature (routing MASM `debug` print events to a custom sink). Inherited upstream changes include note-transport attachment support, a note-screener batch cache, and faster historical-note retrieval. Protocol-layer versions are unchanged (`miden-protocol` / `miden-standards` / `miden-tx` at `0.16.0-alpha.4`).

### Enhancements

* [FEATURE][web] `debugMode: true` surfaces `debug.*` MASM output in the **browser** console (previously browser debug output went nowhere). Output appears in the client's Web Worker console, or the page console when created with `useWorker: false`. This re-introduces `ClientOptions.debugMode`, which `0.16.0-alpha.1` removed: upstream has no runtime debug-mode toggle any more, so the flag now selects the debug-routing transaction executor per execution instead of configuring the client builder. It is browser-only — the Node SDK writes `debug.*` to process stdout regardless — and the routing executor also forwards the advice-stack and advice-map printers, which can expose witness data, so keep it off in production. ([#224](https://github.com/0xMiden/web-sdk/issues/224))
* [FEATURE][react] `MidenConfig` accepts `debugMode?: boolean`, so `<MidenProvider config={{ debugMode: true }}>` routes `debug.*` output to the browser console, matching `ClientOptions.debugMode` on the web client. ([#224](https://github.com/0xMiden/web-sdk/issues/224))

## 0.16.0-alpha.1 (2026-07-19)

### Changes
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@ module_name_repetitions = "allow" # Many triggers,
must_use_candidate = "allow" # This marks many fn's which isn't helpful.
should_panic_without_expect = "allow" # We don't care about the specific panic message.
# End of pedantic lints.

# TEMPORARY: points the client crates at the unreleased rust-sdk rev that carries the
# `debug-output` feature (MASM debug print routing) on top of 0.16.0-alpha.1.
[patch.crates-io]
miden-client = { git = "https://github.com/0xMiden/rust-sdk", rev = "c39d2f0747f0f48233f98e28a6187168f7050929" }
miden-client-sqlite-store = { git = "https://github.com/0xMiden/rust-sdk", rev = "c39d2f0747f0f48233f98e28a6187168f7050929" }
1 change: 1 addition & 0 deletions crates/web-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ browser = [
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
"miden-client/debug-output",
"miden-client/tonic",
]
default = ["browser"]
Expand Down
60 changes: 60 additions & 0 deletions crates/web-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,66 @@ client.terminate();
} // client.terminate() called automatically
```

### Debug mode

Pass `debugMode: true` at client creation to route MASM debug output from
executed scripts to the console. Debug printing goes through the
`miden::core::debug` procedures (`print_stack`, `print_mem`, `print_adv_stack`,
...), which emit events that the debug-routing executor forwards. Browser builds
execute through a debug-routing executor when the flag is set; that executor
also routes the advice-stack and advice-map printers, which can expose witness
data, and is markedly slower, so leave the flag off in production.

```typescript
const client = await MidenClient.create({
rpcUrl: "devnet",
debugMode: true,
});
```

Where that output appears depends on the build:

- **Node** (`@miden-sdk/node-*`): written to the Node process stdout, so it
appears in the terminal running your app. The Node executor does this
unconditionally, so `debugMode` is accepted for parity but changes nothing
there.
- **Browser** (`@miden-sdk/miden-sdk`, including the `/mt` bundle): written to
the browser console. When the client runs in its default Web Worker the
output appears in that worker's console; pass `useWorker: false` to run on the
main thread so it appears in the page console.

For example, executing a script that calls `debug::print_stack`:

```typescript
const script = await client.compile.txScript({
code: `
use miden::core::debug
use miden::core::sys

@transaction_script
pub proc main
push.1.2.3
exec.debug::print_stack
exec.sys::truncate_stack
end
`,
});
await client.transactions.executeProgram({ account: accountId, script });
```

prints the top three stack elements to the console:

```text
Stack state in interval [0, 2] before step 2419:
├── 0: 3
├── 1: 2
├── 2: 1
└── (16 more items)
```

Debug-mode execution is markedly slower than normal execution, so keep it off in
production and enable it only while debugging locally.

## License

This project is licensed under the MIT License - see the LICENSE file for details.
142 changes: 142 additions & 0 deletions crates/web-client/js/__tests__/checkMethodClassification.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { describe, it, expect } from "vitest";
import {
extractWasmMethods,
extractWasmFunctions,
extractClassifications,
extractExplicitMethods,
computeUnclassified,
computeUnknownClassified,
allowedUnclassified,
} from "../../scripts/check-method-classification.js";

// A minimal stand-in for the wasm-bindgen `.d.ts`: a WebClient class with a
// couple of methods (instance + static) plus module-level free functions in
// both the direct and the aliased export form that wasm-bindgen emits.
const WASM_DTS = `
export class WebClient {
free(): void;
createClient(rpcUrl?: string): Promise<any>;
newAccount(account: any, overwrite: boolean): Promise<any>;
getAccounts(): Promise<any>;
static buildSwapTag(a: any): any;
}
export function importStore(store_name: string, store_dump: string): Promise<void>;
declare function exportStore2(store_name: string): Promise<any>;
export { exportStore2 as exportStore };
`;

const INDEX_JS = `
const SYNC_METHODS = new Set([
"buildSwapTag",
]);
const WRITE_METHODS = new Set([
"newAccount",
]);
const READ_METHODS = new Set([
"getAccounts",
"exportStore",
]);

class WebClient {
async newWallet() {}
terminate() {}
}
class MockWebClient extends WebClient {
async syncState() {}
}
`;

describe("check-method-classification extractors", () => {
it("extracts WASM WebClient methods (instance and static)", () => {
const methods = extractWasmMethods(WASM_DTS);
expect(methods.has("createClient")).toBe(true);
expect(methods.has("newAccount")).toBe(true);
expect(methods.has("getAccounts")).toBe(true);
expect(methods.has("buildSwapTag")).toBe(true);
// Free functions are not class methods.
expect(methods.has("importStore")).toBe(false);
expect(methods.has("exportStore")).toBe(false);
});

it("extracts module-level free functions, including aliased exports", () => {
const fns = extractWasmFunctions(WASM_DTS);
expect(fns.has("importStore")).toBe(true);
// `export { exportStore2 as exportStore }`: the PUBLIC name is recorded.
expect(fns.has("exportStore")).toBe(true);
// Class methods are not free functions.
expect(fns.has("newAccount")).toBe(false);
});

it("parses the three classification sets from index.js", () => {
const sets = extractClassifications(INDEX_JS);
expect([...sets.syncMethods]).toEqual(["buildSwapTag"]);
expect([...sets.writeMethods]).toEqual(["newAccount"]);
expect([...sets.readMethods].sort()).toEqual([
"exportStore",
"getAccounts",
]);
});

it("extracts explicit JS wrapper methods from WebClient and MockWebClient", () => {
const explicit = extractExplicitMethods(INDEX_JS);
expect(explicit.has("newWallet")).toBe(true);
expect(explicit.has("terminate")).toBe(true);
expect(explicit.has("syncState")).toBe(true);
});
});

describe("check-method-classification forward check", () => {
it("passes when every WASM method is classified or allow-listed", () => {
const wasmMethods = extractWasmMethods(WASM_DTS);
const sets = extractClassifications(INDEX_JS);
const classified = new Set([
...sets.syncMethods,
...sets.writeMethods,
...sets.readMethods,
]);
// `createClient` and `free` are covered by the allow-list.
const unclassified = computeUnclassified(
wasmMethods,
classified,
allowedUnclassified
);
expect(unclassified).toEqual([]);
});

it("flags a WASM method that is neither classified nor allow-listed", () => {
const wasmMethods = new Set(["newAccount", "brandNewWriteMethod"]);
const classified = new Set(["newAccount"]);
const unclassified = computeUnclassified(
wasmMethods,
classified,
allowedUnclassified
);
expect(unclassified).toEqual(["brandNewWriteMethod"]);
});
});

describe("check-method-classification reverse check", () => {
it("passes when every classified name maps to a real WASM export", () => {
const wasmMethods = extractWasmMethods(WASM_DTS);
const wasmFunctions = extractWasmFunctions(WASM_DTS);
const validExports = new Set([...wasmMethods, ...wasmFunctions]);
const sets = extractClassifications(INDEX_JS);
// exportStore is a free function, buildSwapTag a static method (both valid).
expect(computeUnknownClassified(sets, validExports)).toEqual([]);
});

it("flags a classified name that matches no WASM export (dead entry)", () => {
const wasmMethods = extractWasmMethods(WASM_DTS);
const wasmFunctions = extractWasmFunctions(WASM_DTS);
const validExports = new Set([...wasmMethods, ...wasmFunctions]);
const sets = {
syncMethods: new Set(["buildSwapTag", "setDebugMode"]),
writeMethods: new Set(["newAccount", "forceImportStore"]),
readMethods: new Set(["getAccounts", "exportStore"]),
};
expect(computeUnknownClassified(sets, validExports).sort()).toEqual([
"forceImportStore",
"setDebugMode",
]);
});
});
94 changes: 94 additions & 0 deletions crates/web-client/js/__tests__/clientCreateDebugMode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Guards the positional wiring of `ClientOptions.debugMode` from `MidenClient.create()` down to
// the WASM `createClient` / `createClientWithExternalKeystore` constructors.
//
// The flag travels through a long positional argument list on both constructors. A dropped or
// shifted argument does not fail to compile and does not fail any type check — it just silently
// leaves debug mode off, so `debug.*` MASM output never appears. The end-to-end Playwright test
// (test/debug_output.test.ts) covers the real behavior but needs a live node; these assertions
// pin the argument positions cheaply.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { MidenClient } from "../client.js";

// Positions per crates/web-client/js/index.js, which mirror the wasm-bindgen signatures in
// dist/st/crates/miden_client_web.d.ts.
const CREATE_DEBUG_MODE_INDEX = 4;
const EXTERNAL_KEYSTORE_DEBUG_MODE_INDEX = 7;

describe("MidenClient.create debugMode wiring", () => {
let createClient;
let createClientWithExternalKeystore;
let savedWasmWebClient;
let savedGetWasm;

beforeEach(() => {
createClient = vi.fn().mockResolvedValue({});
createClientWithExternalKeystore = vi.fn().mockResolvedValue({});

savedWasmWebClient = MidenClient._WasmWebClient;
savedGetWasm = MidenClient._getWasmOrThrow;

MidenClient._WasmWebClient = {
createClient,
createClientWithExternalKeystore,
};
// createDevnet/createTestnet default a proverUrl, which resolves through the wasm module.
MidenClient._getWasmOrThrow = vi.fn().mockResolvedValue({
TransactionProver: { newRemoteProver: vi.fn().mockReturnValue({}) },
});
});

afterEach(() => {
MidenClient._WasmWebClient = savedWasmWebClient;
MidenClient._getWasmOrThrow = savedGetWasm;
});

it("forwards debugMode: true to createClient in the expected position", async () => {
await MidenClient.create({ rpcUrl: "devnet", debugMode: true });
expect(createClient).toHaveBeenCalledOnce();
expect(createClient.mock.calls[0][CREATE_DEBUG_MODE_INDEX]).toBe(true);
});

it("forwards debugMode: false rather than coercing it to undefined", async () => {
await MidenClient.create({ rpcUrl: "devnet", debugMode: false });
expect(createClient.mock.calls[0][CREATE_DEBUG_MODE_INDEX]).toBe(false);
});

it("leaves debugMode undefined when the option is omitted", async () => {
await MidenClient.create({ rpcUrl: "devnet" });
expect(createClient.mock.calls[0][CREATE_DEBUG_MODE_INDEX]).toBeUndefined();
});

it("forwards debugMode on the external-keystore path in the expected position", async () => {
const keystore = {
getKey: vi.fn(),
insertKey: vi.fn(),
sign: vi.fn(),
};
await MidenClient.create({ rpcUrl: "devnet", debugMode: true, keystore });
expect(createClientWithExternalKeystore).toHaveBeenCalledOnce();
expect(createClient).not.toHaveBeenCalled();
expect(
createClientWithExternalKeystore.mock.calls[0][
EXTERNAL_KEYSTORE_DEBUG_MODE_INDEX
]
).toBe(true);
});

it("does not displace the keystore callbacks that precede debugMode", async () => {
const keystore = {
getKey: vi.fn(),
insertKey: vi.fn(),
sign: vi.fn(),
};
await MidenClient.create({ rpcUrl: "devnet", debugMode: true, keystore });
const args = createClientWithExternalKeystore.mock.calls[0];
expect(args[4]).toBe(keystore.getKey);
expect(args[5]).toBe(keystore.insertKey);
expect(args[6]).toBe(keystore.sign);
});

it("forwards debugMode through createDevnet's option defaults", async () => {
await MidenClient.createDevnet({ debugMode: true, autoSync: false });
expect(createClient.mock.calls[0][CREATE_DEBUG_MODE_INDEX]).toBe(true);
});
});
Loading