Merge main into next: forward-port the 0.14.x line onto the 0.15 surface#177
Merged
Conversation
* docs: import planning docs from miden-client (web SDK ones) Per @SantiagoPittella's review on miden-client #1992: 5 of the 13 stray planning .md files at the miden-client repo root are web-SDK- relevant — they belong here, not in the Rust client repo. Importing under docs/planning/ with an index README that calls out their snapshot-not-maintained status. AGENTS.md Api.Rework.Impl.md REACT_SDK_PLAN.md SimplifiedAPI.md SimplifiedAPI.sdk.review.md The miden-client side (#1992 commit e20e6261a) deletes all 13 stray docs; the other 8 are either already-tracked issues, stale, or non- client concerns and don't need to be ported. * docs: keep only AGENTS.md from imported planning docs * ci: replace paths-ignore with intra-job filter so docs-only PRs aren't blocked by required checks that never run
* chore: add .nvmrc + lefthook + lint-staged for local dev hygiene * chore: fix lefthook config — drop broken per-file tsc, use pnpm exec, guard prepare
* chore(web-client): swap chai expect for @playwright/test expect * chore(web-client): swap puppeteer Page type for @playwright/test * chore(web-client): drop mocha/chai/puppeteer/esm/ts-node devDeps + .mocharc.json The legacy mocha-based Playwright wiring has been fully replaced by @playwright/test (assertions) and vitest (unit tests). Remove the dead test infrastructure: - crates/web-client/.mocharc.json (deleted) - crates/web-client/tsconfig.json: drop the "ts-node" block - crates/web-client/package.json devDependencies removed: - mocha - chai - puppeteer - esm - ts-node - pnpm-lock.yaml regenerated Verified: pnpm --filter @miden-sdk/miden-sdk run test:unit passes (295 tests across 13 files).
* chore(deps): bump miden-client to 0.14.5
Bumps the workspace miden-client dependency on `main` from 0.14.0 (locked
at 0.14.4) to 0.14.5, the latest 0.14.x patch on crates.io. No API
changes required on the web-sdk side — same minor line, just patch-level
updates from upstream miden-client.
The `next` branch separately tracks miden-client `next` (0.15.0
unreleased) and is unaffected by this bump.
* docs: clarify lazy entry — SSR / controlled WASM init, not 'most users skip crypto'
The previous README framing of `@miden-sdk/miden-sdk/lazy` as for 'apps
where most users never touch crypto' is misleading. The actual point of
the lazy entry is to allow usage in environments that hang on top-level
await (Next.js / SSR, Capacitor WKWebView) or where the caller wants to
explicitly control when the WASM-init cost is paid.
Also adds the missing `await MidenClient.ready()` contract: until you
call it, every wasm-bindgen type imported from the lazy entry is just a
stub that throws on construction. Async SDK methods await internally
and are exempt.
* docs: add React-side lazy guidance — gate UI on isReady from useMiden()
Apps using @miden-sdk/react with the lazy entry don't call
MidenClient.ready() directly; MidenProvider does it for them and
exposes the readiness state through useMiden() as { isReady,
isInitializing, error }. Adds the contract + a minimal example,
plus pointers to the loadingComponent / errorComponent provider
props for the zero-glue case.
* chore(ci): add publint + arethetypeswrong gates for published packages
Adds publint and @arethetypeswrong/cli as root devDeps and wires three
new scripts:
- check:publint - runs publint per published workspace package
- check:attw - runs attw --pack . per published workspace package
- check:publish - builds web-client, react-sdk, vite-plugin then runs
both gates
Filters target the three publishable packages by name
(@miden-sdk/miden-sdk, @miden-sdk/react, @miden-sdk/vite-plugin), which
skips the private web_store workspace member that has no exports map
worth checking.
A new workflow (.github/workflows/check-publish.yml) runs check:publish
on PRs and pushes to main/next. It mirrors the WASM build setup from
test.yml's build-web-client-dist-folder job (Rust toolchain, sccache,
binaryen, Swatinem/rust-cache) and uses MIDEN_FAST_BUILD on the WASM
build since publint/attw inspect package shape and types, not the
optimization level of the WASM blob.
Mode: STRICT. Per the task rule (document only if both tools find > ~3
issues per package; otherwise leave strict), only @miden-sdk/miden-sdk
clearly clears that bar - the other two packages have <= 1 attw issue
and 1 publint warning each, so a doc-and-tolerate path would be
overkill. The CI gate will be red on first PR; the author should decide
whether to fix the underlying export map issues or merge red.
Local results today (built with MIDEN_FAST_BUILD=true):
publint (exit 0 - warnings/suggestions only):
- @miden-sdk/miden-sdk: 1 suggestion (pkg.browser refactor)
- @miden-sdk/react: 1 warning + 2 suggestions
(types ambiguous under "import" condition;
missing "type" field; repository.url shape)
- @miden-sdk/vite-plugin: 1 warning + 2 suggestions (same shape as react)
attw (exit 1 - real failures):
- @miden-sdk/miden-sdk: CJSResolvesToESM, InternalResolutionError
across multiple matrix cells, NoResolution
on the /lazy subpath under node10
- @miden-sdk/react: FalseCJS (masquerading-as-CJS) under node16
from-ESM for both . and /lazy; NoResolution
on /lazy under node10
- @miden-sdk/vite-plugin: FalseCJS under node16 from-ESM
Per task scope, no source or build-config changes were made to address
these - the gate ships first, the fixes are followup work.
* chore(miden-sdk): fix exports map for attw + publint compliance
- Split the `exports` conditions into explicit `import` blocks with
`types` listed first, eliminating the ambiguous-types warning publint
reported. Drop the implicit `default` fallthrough that caused
attw to flag CJSResolvesToESM under the node16-cjs profile.
- Add an `.attw.json` selecting the `esm-only` profile. The package is
`"type": "module"` and ships only ESM artifacts (rollup output +
WASM glue); `require()` consumers must already use a dynamic import.
The `esm-only` profile communicates this intent to attw and makes
the node10 / node16-cjs columns informational only.
- Add a post-build step (`scripts/post-build.js`) that:
1. Rewrites extensionless relative specifiers in the published
`dist/*.d.ts` files (e.g. `from "./api-types"` ->
`from "./api-types.js"`). Without explicit extensions, Node16
type resolution flags an `InternalResolutionError` on every
relative import inside `dist/index.d.ts` and `dist/api-types.d.ts`.
2. Emits a `lazy/package.json` shim at the package root pointing at
`dist/index.{js,d.ts}` so node10 resolution (which doesn't read
`exports`) can still locate `@miden-sdk/miden-sdk/lazy`.
- Add `lazy` to the `files` array so the shim ships in the tarball.
- Expose `./package.json` from `exports` to keep tooling that reads
the manifest at runtime working under strict resolution.
Public import paths (`@miden-sdk/miden-sdk` and
`@miden-sdk/miden-sdk/lazy`) are unchanged. Verified that the
existing vitest unit suite (295 tests) still passes against the new
build output.
* chore(react-sdk): fix exports map for attw + publint compliance
- Split each `exports` subpath into explicit `import` / `require`
conditions with `types` listed first. publint flagged the previous
shape because `types: "./dist/index.d.ts"` resolved as CJS under the
`import` condition (FalseCJS / ambiguous-types). The new shape uses
the `.d.mts` declaration tsup already emits for the ESM build, so
TypeScript sees an `.mts` declaration when resolving via `import`
and a `.d.ts` declaration when resolving via `require`.
- Add `"type": "commonjs"` to silence publint's package-type-detection
suggestion (tsup emits `.js` as CJS and `.mjs` as ESM, matching).
- Add a `lazy/package.json` shim at the package root that points at
`dist/lazy.{js,mjs,d.ts}`. node10 resolution doesn't read the
`exports` map, so `@miden-sdk/react/lazy` previously failed to
resolve under that column; the shim is the standard fix.
- Expose `./package.json` from `exports` and add `lazy` to the `files`
array so the shim ships in the tarball.
Public import paths (`@miden-sdk/react` and `@miden-sdk/react/lazy`)
are unchanged. Verified that the existing vitest unit suite (719
tests) still passes.
* chore(vite-plugin): fix exports map for attw + publint compliance
- Split the `exports["."]` block into explicit `import` / `require`
conditions with `types` listed first. publint flagged the previous
shape because `types: "./dist/index.d.ts"` was interpreted as CJS
when resolving via the `import` condition (FalseCJS / ambiguous
types under `import`). The new shape uses the `.d.mts` declaration
tsup already emits for the ESM build, so TypeScript sees an
`.mts` declaration when resolving via `import` and a `.d.ts`
declaration when resolving via `require`.
- Add `"type": "commonjs"` to silence publint's package-type-detection
suggestion (tsup emits `.js` as CJS and `.mjs` as ESM, matching).
Public import path (`@miden-sdk/vite-plugin`) is unchanged. Verified
the existing vitest suite (27 tests) still passes.
* chore(react-sdk): drop CJS output, ship ESM-only
* chore(web-client): deduplicate lazy/package.json shim
* chore(ci): prettier-format post-build.js
* chore(react-sdk): rename CJS configs to .cjs after ESM-only flip
Switching @miden-sdk/react to "type": "module" makes .js files ESM by
default, which breaks two CJS-syntax files:
- packages/react-sdk/eslint.config.js (uses module.exports + require())
- packages/react-sdk/test/serve-tests.js (uses require())
Renamed both to .cjs. Updated playwright.config.ts:34 to reference the
new filename. ESLint auto-discovers either extension; no other consumer
references the renamed paths.
* chore: add vitest workspace + root test script * chore(vitest): migrate from defineWorkspace to defineConfig projects (vitest 3 modern shape) * chore(ci): exclude root vitest.config.ts from eslint typed-linting
* chore: add knip for unused-exports/deps detection
Lands knip 6.7.0 plus a baseline knip.jsonc config covering the four
TS-bearing workspaces (`packages/react-sdk`, `packages/vite-plugin`,
`crates/web-client`, `crates/idxdb-store/src`) and a root scripts entry.
Mode: warning-only. The script is `knip --no-exit-code` so CI does not
go red on day-one findings; promoting to strict mode is a follow-up
once the baseline backlog is cleared.
Baseline findings (pnpm run check:knip, exit 0 in --no-exit-code):
Unused files 4 (3 are .d.ts ambient declarations in
crates/web-client/js/types/, plus
packages/react-sdk/src/__tests__/utils/test-utils.tsx)
Unused dependencies 3 (root prettier; web-client @rollup/plugin-typescript, dexie)
Unused devDeps 5 (root and idxdb-store @typescript-eslint/eslint-plugin;
web-client http-server + mocha;
react-sdk http-server)
Unlisted dependency 1 (web-client test references @aspect-build/aspect-rsdoctor)
Unlisted binary 1 (vite in .github/workflows/wallet-pages.yml)
Unresolved imports 4 (web-client tests import ./eager.js / ./index.js
relative paths — likely dist-time URLs)
Unused exports 23 (all in packages/react-sdk test mocks)
Unused exported types 9 (react-sdk SignerContext + types/index.ts re-exports)
Duplicate exports 3 (playwright.global.setup.ts; react-jsx-runtime.js;
vite-plugin index.ts midenVitePlugin|default)
Per-workspace tally:
root 1 dep, 1 devDep
packages/react-sdk 1 file, 1 devDep, 23 exports, 9 types, 1 dup-export
packages/vite-plugin 1 dup-export
crates/web-client 3 files, 2 deps, 2 devDeps, 1 unlisted dep,
1 unlisted bin, 4 unresolved imports, 1 dup-export
crates/idxdb-store/src 1 devDep
No CI workflow added in this commit — that lands separately once the
baseline is cleaned up. No source code modified.
* chore(web-client): drop dead @aspect-build import in sync_lock test
The 'waiters are rejected when sync times out' test destructured
acquireSyncLock/releaseSyncLock/releaseSyncLockWithError from a stale
@aspect-build/aspect-rsdoctor path that has nothing to do with this
codebase, then immediately fell back to nulls and never used the
bindings. The only thing actually exercised below is client.syncState(),
so the destructuring + bogus import was pure dead code.
Flagged by knip as the sole 'unlisted dependency' import in
crates/web-client; removing the dead block both fixes the finding and
makes the test honest about what it's checking.
* chore: drop unused devDeps + deps flagged by knip
Removes packages that no source file, build config, lint config, test, or
script references. Verified each by grepping the repo for imports / CLI
invocations / config references before deletion.
- root: @typescript-eslint/eslint-plugin (root eslint.config.js wires
parser only; no plugin-rules block, so the plugin pkg sits idle)
- crates/idxdb-store/src: @typescript-eslint/eslint-plugin (its
eslint.config.mjs uses 'typescript-eslint' meta-package, not the
legacy plugin)
- packages/react-sdk: http-server (no script or workflow runs it)
- crates/web-client: http-server, mocha (rollup + playwright + vitest
pipeline does not invoke either; no .mocharc, no http-server script)
- crates/web-client deps: @rollup/plugin-typescript (rollup.config.js
uses node-resolve + commonjs + wasm-tool only — no TS plugin), dexie
(only the idxdb-store crate uses dexie, and it lists its own copy)
Lockfile regenerated via pnpm install --no-frozen-lockfile.
* chore: drop unused files + dead exports flagged by knip
react-sdk:
- Delete src/__tests__/utils/test-utils.tsx — exports renderWithProvider,
renderHookWithProvider, etc., none of which any test imports. The repo
uses renderHook/render directly from @testing-library/react.
- Strip 'export' off internal mock helpers and types in __tests__/mocks/
(createMockOutputNote, createMockTransactionRecord, MockNoteFilter and
~17 other Mock* constants, MockWebClientType, createMockSignCallback,
createMockAccountStorageMode). They were never imported across module
boundaries; making them module-private is correct.
- Delete the createMockSdkModule factory along with the Mock* class /
enum constants that only existed to populate it. No test in the repo
ever called the factory, so it pulled an entire wing of dead code.
- Remove unreachable type re-exports: GetKeyCallback / InsertKeyCallback
/ NoteId / NoteVisibility / StorageMode were re-exported through
src/types/index.ts but src/index.ts never re-exported them, so they
were not part of the package's public surface. The underlying types
stay locally-typed where they're actually used.
- Make ClientWithTransactions in src/utils/transactions.ts module-private
(the noteFilters.ts copy is the one consumers use).
web-client:
- Delete crates/web-client/.mocharc.json (only consumer was mocha, which
was removed in the previous commit) and the matching ts-node /
esm devDeps + the now-orphan ts-node block in tsconfig.json.
Public API of @miden-sdk/react is unchanged: every type still exported
through src/index.ts continues to be exported. Only types that were
never reachable through the package entry have been demoted.
* chore(knip): allowlist public-API exports + ambient .d.ts files
After deleting actual dead code, the remaining findings are all cases
where knip can't statically see the consumer:
- crates/web-client/js/types/{index,api-types,docs-entry}.d.ts: shipped
as the package's published types via 'cpr js/types dist' in the build
script, so they ARE the consumer of themselves at publish time.
Registered as entry points with a comment naming the post-build copy
step.
- ./eager.js, ./index.js: dynamic imports inside page.evaluate()
callbacks, executed in the browser against http://localhost:8080
(i.e. the dist/ output served by the test http server). Resolved at
test runtime, not relative to the Playwright test file. Allowlisted
via ignoreUnresolved with a comment.
- ./crates/miden_client_web: wasm-bindgen module emitted into dist/ by
the rollup rust plugin; the .d.ts files reference it but it doesn't
exist until after build. Same allowlist with a comment.
- prettier: invoked from the repo Makefile via 'pnpm exec prettier .';
knip doesn't scan Makefile rules. Allowlisted in ignoreDependencies.
- vite: invoked from .github/workflows/wallet-pages.yml via 'pnpm exec
vite build' inside the wallet example workspace, which has its own
package.json + lockfile not visible to this monorepo's graph.
Allowlisted in ignoreBinaries.
- Three intentional dual-export sites (vite plugin named+default, the
Playwright test fixture's named+default 'test', and the React JSX
runtime shim's jsx/jsxs/jsxDEV aliases): rules.duplicates set to
'off' globally with a comment enumerating each case so a future
change knows when to revisit it.
Also dropped the redundant entry patterns and the empty docs/** ignore
patterns that knip flagged as configuration hints.
After this, 'pnpm exec knip' exits 0 with zero findings; the 3 remaining
'configuration hints' are about react-sdk's package.json exports
pointing at not-yet-built dist/lazy.* files, which is correct and
doesn't affect the exit code.
* chore(knip): flip check:knip to strict mode
All baseline findings have been triaged in the preceding commits, so the
warning-only flag is no longer needed. Knip now fails CI on any new
unused export / dep / file / unresolved import.
* chore(ci): wire knip into lint workflow
* chore(ci): restore dexie + prettier-format knip files
Knip's static scan flagged dexie as unused, but it's bundled into the
web-client test page at runtime via rollup — page.evaluate blocks load
the bundle from localhost:8080 which transitively imports dexie. The
removal broke 22 integration tests with "Failed to resolve module
specifier 'dexie'". Re-added dexie@^4.0.1 to crates/web-client deps
and registered it in knip's top-level ignoreDependencies with a comment.
Also runs prettier --write on knip.jsonc and two react-sdk source
files that were missed in the earlier cleanup pass.
* chore(knip): allowlist publint/attw + .cjs serve-tests rename
Removes overlapping formatting rules (semi, comma-dangle, eol-last, space-before-blocks, keyword-spacing, no-multiple-empty-lines) from the root eslint.config.js so Prettier 3.x is the single source of truth for style. Logic rules (camelcase, @typescript-eslint/no-unused-vars) are preserved. Adds eslint-config-prettier as the last entry in both flat configs (eslint.config.js and packages/react-sdk/eslint.config.js) to disable any remaining stylistic rules pulled in transitively from rule presets, preventing eslint and prettier from fighting. Extends .prettierignore to mirror the eslint configs' ignore list (dist, target, node_modules, generated .d.ts, docs, idxdb-store codegen) so prettier --check stays scoped to source we actually own. The existing .prettierrc.json (trailingComma: es5) is left as-is. Verified: pnpm --filter @miden-sdk/react run lint shows 0 errors (same 5 pre-existing warnings as main); prettier --check on packages/react-sdk and packages/vite-plugin source passes with no reformatting needed.
…#35) The pre-split miden-client release workflow shipped all three packages on a tagged release; the web-sdk port dropped react-sdk and vite-plugin publishing while keeping web-client. End users `npm install @miden-sdk/react` were therefore stuck on the `next` dist-tag (set by publish-web-client-next.yml) instead of getting the latest tagged release. This restores parity with pre-split behaviour: - New scripts/check-react-sdk-version-release.sh and scripts/check-vite-plugin-version-release.sh — same pattern as the existing check-web-client-version-release.sh (compare package.json version against the tagged commit's parent, set should_publish flag). - publish-web-client-release.yml now runs each package's version-bump check and conditionally builds + publishes that package. The three publishes are independent, so a release that bumps only one package republishes only that one. - The web-client build runs whenever EITHER web-client OR react-sdk needs to ship, since react-sdk's build consumes the WASM dist.
Match the secret name to the cargo env var (and to miden-node's convention) so the publish-crates workflow picks up the org-managed secret without an indirection.
…ale AGENTS (#38) - Add a top-level CLAUDE.md aimed at AI agents (and humans skimming for build/lint/test/release conventions). Captures pnpm-only rule, the Makefile-driven workflows, runExclusive, the eager/lazy entry contract, the npm-registry-driven publish gate, and cross-repo coordination notes. The README already links to it (line 337). - Refresh packages/react-sdk/CLAUDE.md against the current source: * useNotes returns notes/consumableNotes (not input/consumable) * useAccounts returns accounts/wallets/faucets (no 'all') * Mutation hooks expose action-named callbacks (send, mint, ...) and a 'result' field — not generic { mutate, data } * useSend / useMultiSend take assetId / recipients (not faucetId / outputs) * SendResult has txId (not transactionId) * SignerAccountConfig uses publicKeyCommitment + accountType * Hook reference table reorganized into query/mutation buckets and includes the previously-undocumented hooks (useNoteStream, useSyncControl, useTransactionHistory, useImportNote / Export*, etc.) - Remove docs/planning/AGENTS.md — 7 lines of miden-client leftover that referenced 'yarn prettier' (the repo migrated to pnpm). All the still-useful content is now in the top-level CLAUDE.md.
* ci(changelog): add changelog gate + filtered web-sdk CHANGELOG.md Mirror miden-client's changelog gate. Each PR must touch one of the three changelog files; trivial PRs override with the 'no changelog' label. Files: - CHANGELOG.md (new) — pre-filled with miden-client's [web]-tagged entries up through 0.14.x. Source-of-truth changelog for the WASM client (@miden-sdk/miden-sdk). - packages/vite-plugin/CHANGELOG.md (new, stub) — placeholder so the CI script has a real path to diff against. - packages/react-sdk/CHANGELOG.md (already exists) — unchanged here. - scripts/check-changelog.sh (new) — mirrors miden-client's script, extended with a third path for the vite-plugin. - .github/workflows/changelog.yml (new) — fires on PR open / sync / label changes. * ci(changelog): collapse to single root CHANGELOG.md + auto-populate release notes - Drop packages/react-sdk/CHANGELOG.md and the packages/vite-plugin/ CHANGELOG.md stub. The single root CHANGELOG.md is the source of truth for every published artifact in this repo. - Simplify scripts/check-changelog.sh accordingly: only the root file is checked. The 'no changelog' label still overrides. - Add scripts/extract-changelog-section.sh: takes a version (with or without a leading 'v'), prints the matching '## <version> (...)' section body to stdout. Used by the release-notes workflow. - Add .github/workflows/release-notes.yml: on release:published, looks up the matching CHANGELOG section and replaces the release body via 'gh release edit --notes'. Logs a warning + leaves the body untouched if no matching section is found (no failure — release shouldn't be blocked on a stale changelog; the gate at PR time is where that's enforced).
* ci: migrate heavy jobs to WarpBuild for ~3x speedup * chore: ignore docs/superpowers * chore: remove tracked docs/superpowers (now gitignored)
#46) * fix(ci): wait for node-builder/prover gRPC port instead of sleep+pgrep Root-cause fix for the recurring 'Integration tests (ci-shard-N)' flake that intermittently surfaces as 'TypeError: Failed to fetch' from the in-browser gRPC client during ensure_genesis_in_place. The previous startup pattern was: ./bin/testing-node-builder & sleep 4 pgrep -f testing-node-builder || exit 1 which only checked process liveness, not that the gRPC listener had bound to its TCP port. On slower CI runners, the test job would race the listener and fail the very first RPC call (get_block_header_by_number) with a JS-level fetch failure. Replaced with a /dev/tcp probe loop with a 30s timeout. Same fix applied to all three startup sites: - integration-tests-web-client: testing-node-builder on :57291 - integration-tests-remote-prover-web-client: testing-node-builder on :57291 - integration-tests-remote-prover-web-client: testing-remote-prover on :50051 If the port isn't reachable in 30s the step still fails with a clear error (and prints whether the process is running at all). * fix(ci): wait for node-builder/prover stdout readiness signal, not bare TCP Previous fix (b02c1dc) replaced sleep+pgrep with a TCP-probe loop on :57291. The probe consistently passes within 6s on WarpBuild runners, but tests STILL hit 'TypeError: Failed to fetch' from the in-browser gRPC client. Root cause: the node-builder binds its public TCP listener early but doesn't finish wiring up the gRPC services (store, ntx-builder, block- producer, validator) until `NodeBuilder::start().await` returns. A bare TCP probe to :57291 succeeds before the gRPC service is registered — in-browser fetches race in and fail. The reliable readiness signal is the explicit 'Node started successfully with PID' stdout line printed AFTER start().await returns (see miden-client crates/testing/node-builder/src/main.rs:55). This commit: - Pipes node-builder stdout to /tmp/node-builder.log and waits (timeout 60s) for the readiness line to appear before returning from the step. - Same treatment for the remote-prover step ('Remote prover listening on <addr>' from prover/src/main.rs:87). - On timeout, dumps the last 50 lines of the captured log so the failure mode is visible. * fix(ci): force line-buffered stdout via stdbuf -oL on test backends Addendum to 931e36a. The stdout-grep readiness check on the captured log was timing out because Rust's stdout is BLOCK-buffered when connected to a file (`>/tmp/node-builder.log`) instead of line-buffered when connected to a TTY. The 'Created data directory' line happened to flush, but 'Node started successfully' (printed ~80 bytes later) never reached the buffer fill threshold (~8KB) so it stayed in memory and the grep loop timed out at 60s. Wrap both backend launches with `stdbuf -oL` (coreutils) to force line-buffered stdout, which makes println output visible to the log file as soon as the newline is written. Same fix on the remote prover step. * fix(ci): revert readiness probe over-engineering, add Playwright retries: 1 Backing out 931e36a + a31c792. The root-cause hypothesis (gRPC listener bound before service registered) led to a stdout-grep readiness check that consistently TIMED OUT — start().await either hangs or stdout doesn't flush properly when redirected to a file (stdbuf -oL didn't fix it). All 4 integration shards failed solid on the over-engineered version. Restore the original `sleep 4 && pgrep` startup pattern (which was flaky at maybe 1-3% of runs but functional 97% of the time), and add `retries: 1` to playwright.config.ts on CI to mask the residual flake without doubling test wall-clock. The actual root cause of the original 'TypeError: Failed to fetch' flake remains unidentified — not enough signal in failed runs to isolate it. If it becomes a real problem (>5% of runs), revisit with debug instrumentation in the test harness or the node-builder binary itself rather than the workflow YAML.
* ci(publish): consolidate to single workflow + trusted publishing (main) Replaces the two-workflow setup (publish-web-client-release.yml + publish-web-client-next.yml) with a single publish-web-sdk.yml that handles both the GitHub-release event AND the next-channel patch-release PR-merge event in one file. Why one file: npm's trusted publishing has a (package, repo) uniqueness constraint — only ONE workflow filename per package can be the trusted publisher. Probed empirically: registering trust for a second workflow on the same package returns 409 Conflict, including with a different `--env` scope. To use trusted publishing for both flows, both must come from the same workflow file. Migration to trusted publishing: - permissions.id-token: write at workflow + publish-job level → GitHub mints the OIDC token npm exchanges for a short-lived publish credential. - node-version bumped to 24 + force-update of npm CLI to ensure ≥ 11.5 (required for OIDC support). - NPM_WEBCLIENT_TOKEN env vars dropped from every publish step. - --provenance flag added to every `npm publish` / `pnpm publish` for the signed attestation that ships with the package. Branching by event: - release:published → `--tag latest`, gated by check-*-version-release.sh. - pull_request:closed (against next, with patch-release label) → `--tag next`, gated by check-*-version-pr.sh. Inert on main (branch filter excludes main-targeted PRs); kept here for parity so the same trust config works across both long-lived branches. - vite-plugin still ships only on release events (no pr.sh script for it). Tracked as future work. Trust registration follow-up: each of the 6 published packages has its trust pointing at `publish-web-client-release.yml`. After this lands, huna1 (npm org owner) revokes those + re-registers against `publish-web-sdk.yml` for both branches' workflow location. Until that happens the publish job will fail at the publish step (no trust → 401). * fix(ci/publish): guard skipped job + pin npm to ^11.5 Two review-feedback fixes on the consolidated publish workflow: 1. `skipped:` job no longer fires misleadingly when `check-version` itself was skipped. Previously: a PR merged to `next` without the `patch release` label rejected at `check-version`'s `if:`, all `should_publish_*` outputs were empty strings, the `!= 'true'` checks all evaluated true, and `skipped:` ran printing "All publish gates failed" — even though no gating ever happened. Adding `needs.check-version.result == 'success'` to the guard prevents that. 2. Pin the global npm install to `^11.5` instead of `@latest`. A future npm 12 with breaking changes to `--provenance` / OIDC semantics would silently break this workflow under @latest. The ^11.5 floor still gets the OIDC support we need.
* ci: auto-cut GitHub release on 'patch release' merge to main * docs: changelog entry for auto-release workflow
…ain) (#53) * fix: restore exec bit on check-react-sdk-version-pr.sh * fix(ci): wait for node-builder readiness via pty (real fix) Spawn testing-node-builder + testing-remote-prover via 'script -qfec' (allocates a pty), then poll the captured log for the binary's own 'Node started successfully' / 'Remote prover listening' stdout signal. Why each prior approach failed: - 'sleep 4 && pgrep': only proves the process is alive, not that the port is bound or gRPC handlers are registered. ~10% flake. - TCP probe (/dev/tcp): kernel-level handshake succeeds the moment TcpListener::bind() is called, but tonic's gRPC service dispatcher registers AFTER that. Racy. - 'stdbuf -oL': only modifies libc stdio buffering. Rust's println!() goes through std::io::stdout()'s own BufWriter (~8KB block buffer on non-tty fds) and ignores stdbuf entirely. Captured log stayed empty until the buffer filled, long past the readiness window. The pty is the actual fix: Rust detects the tty fd and constructs stdout as a LineWriter, so println!() flushes on every newline. 'script -qfec' is preinstalled (bsdmainutils) on Ubuntu runners. Centralized into scripts/wait-for-bg-binary.sh so the same probe applies at all 4 spawn sites (3 node-builder + 1 remote-prover). * Revert "fix(ci): wait for node-builder readiness via pty (real fix)" This reverts commit 0ad7700.
…ENT_REF v0.14.4 → v0.14.5 (#59) * fix(ci): introduce wait-for-grpc.sh with strict 3-digit HTTP code check (sibling of next) Sibling fix to next-side. PR #57 (the original main-side flake fix) shipped a probe that false-positived on shard-4: reported 'gRPC dispatch responsive after 1 attempt (HTTP 000000)' and tests then ran against a not-yet-ready server, hitting the original TypeError: Failed to fetch. Two issues: 1. The contrived gRPC-web payload `--data-binary "$(printf '\x00\x00...')"` tripped bash's null-byte stripping in command substitution. The payload was always empty after substitution; the printf was useless. 2. The success check `[ "$http_code" != "000" ]` was too loose. On the failing run curl somehow emitted "000000" (a six-digit string, probably from an internal retry concatenation), which 'is not 000' and the loop broke immediately. Fix: drop the fancy POST, use a plain GET (any HTTP response from tonic — even 404/405/415 — proves the dispatcher is up). Tighten the success regex to require an exact 3-digit [1-5][0-9][0-9] code. Failures and timeouts continue to produce '000' which now correctly fails the regex and loops. Note: PR #57 was never merged. This PR brings the script forward plus the test.yml wiring (the 3 spawn site replacements that were in PR #57). * ci(test): wire wait-for-grpc.sh into all 3 spawn sites * ci: bump MIDEN_CLIENT_REF v0.14.4 -> v0.14.5 The HTTP-probe readiness check (introduced earlier in this PR) works on next branch where MIDEN_CLIENT_REF=dab6cf7b... but breaks main on v0.14.4 — every test fails with TypeError: Failed to fetch even though the probe gets HTTP 200. The older testing-node-builder appears to misbehave after handling a non-gRPC HTTP request. Bump to v0.14.5 to pick up the corresponding stable client release; the probe should be compatible with this slightly newer binary.
Required by npm's provenance verifier. Without it, publishes via
trusted publishing fail with:
422 Unprocessable Entity
Failed to validate repository information: package.json:
"repository.url" is "", expected to match "https://github.com/0xMiden/web-sdk"
react-sdk and vite-plugin already had the field; only web-client
was missing. Discovered when 0.15.0-alpha.3 published the platform
packages but rejected the web-client publish on next.
Observed flake: probe returns HTTP 200 once on the first attempt that clears the connection-refused phase, exits, tests start, ALL tests fail with 'TypeError: Failed to fetch' to the gRPC backend. The single-probe gate isn't strict enough — a one-shot 200 (e.g. tonic-health responding before the rest of the dispatcher is fully wired) currently passes. Upgrade the readiness signal to N consecutive HTTP successes spaced PROBE_INTERVAL apart (defaults: 3 successes, 0.5s apart), so the probe only declares the server ready after ~1s of demonstrably-stable response. Any non-success in the streak resets it to zero and the slow-poll loop resumes — so a momentary blip during init doesn't get counted twice on either side. Tracked occurrences across recent PR runs: web-sdk PR #23 ci-shard-4, PR #29 ci-shard-1 + ci-shard-4, PR #27 multiple shards.
* ci: auto-patch miden-client dep from PR description marker
When a web-sdk PR description contains:
Client PR: #1234
Client PR: 0xMiden/rust-sdk#1234 (cross-repo / fork form)
a new composite action (.github/actions/inject-linked-client-pr) parses
the marker, resolves the linked PR's head ref, and rewrites web-sdk's
miden-client (and miden-client-sqlite-store) dep in place to point at
that branch — only on the runner, never committed. The build then runs
against the unreleased upstream code, while the committed Cargo.toml
diff stays clean. Replaces the manual '[patch]' / branch-edit dance
that this session has been carrying on every migration PR.
Why in-place rewrite instead of [patch.crates-io] / [patch."<url>"]?
Web-sdk's 'next' pins miden-client at git+url=...miden-client.git+
branch=next; pointing the same URL at a different branch via [patch]
errors with 'patches must point to different sources'. In-place
rewrite covers both 'main' (crates.io dep) and 'next' (git dep)
uniformly — the rewritten line is wrapped in marker comments that
preserve the original verbatim so a cleanup step can restore it.
Components:
.github/actions/inject-linked-client-pr/action.yml
Composite action: parse marker, resolve head, validate state
(closed-without-merge fails the run loudly), patch Cargo.toml +
refresh Cargo.lock, post a sticky PR comment summarizing the
patch. Strict 0-or-1 comment invariant: only fires when called
with comment=true (one designated job per workflow run), and
deletes the prior comment if the marker is later removed.
.github/workflows/build.yml + test.yml
Wired the action into the cargo-compiling jobs (build-wasm,
build-web-client-dist-folder, verify-release-build). build-wasm
is the single comment-poster (comment=true).
.github/workflows/check-linked-client-pr.yml
Mergeability gate: keeps a 'linked-client-pr-ready' check on the
PR. Stays pending while the linked client PR isn't merged-and-
reachable from the target branch's canonical refs (miden-client
next for next-targeted PRs, latest miden-client release tag for
main-targeted). Re-evaluates every 15 min so the check goes green
automatically once upstream catches up — no need to re-push.
Configure branch protection to require it.
scripts/dev-with-client-pr.sh
Local-dev mirror: applies the same in-place dep rewrite to your
working tree. Idempotent. '--clear' restores the originals
byte-for-byte from the marker block.
lefthook.yml
Pre-commit guard: refuses any commit while the marker block is
present in Cargo.toml. So a forgotten 'apply' can't accidentally
leak into a commit.
CLAUDE.md
Documents the marker convention, the local script, and the
mergeability gate.
* fix(ci): use POSIX [[:space:]]* instead of \s in marker grep
GNU grep on the runner balked with 'repetition-operator operand invalid'
on the \s* in the regex. \s is a Perl-extension class that grep -E
doesn't recognize in POSIX-extended mode; the * after it ends up
applying to nothing, hence the error. Replace with [[:space:]]* across
the composite action, the dev script, and the readiness gate workflow.
* fix(ci): anchor Client PR marker regex to start-of-line
Without the ^[[:space:]]* prefix, doc examples or table cells in the
PR body that mention 'Client PR:' inline (e.g. inside backticks for a
test-plan checklist) are picked up as if they were real markers — the
action then tries to fetch a fake miden-client PR and fails with
'Failed to fetch'. Anchoring requires the marker to start its own
line, so inline mentions in surrounding prose stay decorative. Doc
examples that use placeholders like #<NUM> instead of #N (a literal
number) are also rejected by the existing #[0-9]+ requirement.
Pairs with the PR-body cleanup that replaces the literal example
'Client PR: #1234' with 'Client PR: #<NUM>' so it's clearly a template.
gh's REST API returns pull-request state as 'open' / 'closed', GraphQL returns 'OPEN' / 'CLOSED'. The closed-PR guardrail in the composite action and the dev script were comparing against the uppercase form, so the closed-without-merge branch always fired even on open PRs — the action exited with 'is closed without merge' on every run with a 'Client PR:' marker. Normalize the API-returned value via tr to lowercase, then compare to 'open'. Verified against miden-client#2059 (state='open' from REST).
…ment (#69) The auto-patch action's parse + Cargo.toml-rewrite steps succeed, but the sticky-comment step fails with 'Resource not accessible by integration' because build.yml only declares contents: read at the workflow level. The error short-circuits the composite action and the build job fails before cargo even runs. Two-part fix: build.yml -> grant pull-requests: write to the build-wasm job only. Scoped to that job (rather than at the workflow level) so every other job keeps the read-only default. Principle of least privilege: only the comment-poster needs the write. inject-linked-client-pr/action.yml -> mark the sticky-comment steps (both upsert and delete) as continue-on-error: true. The comment is decorative — if write permissions ever go missing again, or if the sticky-comment marketplace action has a transient blip, we shouldn't abort the rewrite step's gains. The patch is already on disk at the point those steps run.
PR #25's verification run surfaced a coverage gap: my initial wiring in #65 only patched build.yml's build-wasm and test.yml's build-web-client-dist-folder + verify-release-build. But Clippy WASM (in lint.yml) and publint + attw (in check-publish.yml) ALSO compile cargo against the workspace's miden-client dep — without the patch, they hit canonical crates.io `miden-client = "0.14.5"` and fail with `error[E0599]: no variant named `ApplyTransactionAfterSubmitFailed`` when the PR depends on an unreleased upstream variant. Add the action call after checkout in both jobs. Both run with comment=false (the default); build-wasm remains the single sticky- comment poster.
…#73) Two issues from PR #25's verification run: 1. The 'linked-client-pr-ready' custom commit status the gate posts never landed. Cause: the workflow's permissions block has 'checks: write' (for check-runs) but not 'statuses: write' (which the POST /repos/.../statuses/$sha endpoint requires). The gh api POST 403'd silently — and #2 below ate the error. 2. set_status() redirected gh's stdout to /dev/null. With no error visible, the silent 403 looked like 'job ran successfully' even though the readiness verdict never reached the commit. This commit: - adds 'statuses: write' to the workflow permissions - drops the >/dev/null redirect so any future API failure is loud
#75) Previous form: read -r body base_ref state <<<"$(gh api ... --jq '"\(.body // "")\t\(.base.ref)\t\(.state)"')" PR bodies are multi-line and the marker is itself space-separated. read defaults to whitespace IFS and only consumes through the first newline, so a body starting with 'Client PR: #2059\n...' assigned: body = 'Client' base_ref = 'PR:' state = '#2059' The gate then hit the early-return: if [ "$state" != "open" ] && [ "$state" != "OPEN" ]; then echo "PR #$PR_NUM is $state — skipping." exit 0 fi …and exited with the visible-in-the-job 'PR #25 is #2059 — skipping.', never posting the linked-client-pr-ready custom status. The job logged 'success' because exit 0 is what the early-return does, but the readiness verdict never reached the commit. Fix: - body = separate gh api call (multi-line tolerated naturally) - base_ref + state packed into a tab-separated single-line, read with IFS=$'\t' so a hypothetical future whitespace in either field wouldn't repeat the bug - Same IFS=$'\t' added to the second read of (merged, merge_commit_sha) for consistency, even though neither field can contain whitespace today
…arker (#77) The README's Contributing section was a stub. Move it into a proper CONTRIBUTING.md (GitHub auto-detects this filename and surfaces it on new-issue / new-PR pages) and expand it with the cross-repo workflow: - The 'Client PR: #N' marker convention (and the cross-repo / fork form 'Client PR: 0xMiden/miden-client#N') - What CI does with the marker (auto-patch action, sticky PR comment, readiness gate) - Branch-protection guidance (require linked-client-pr-ready, NOT the gate matrix job) - Local-dev parity (scripts/dev-with-client-pr.sh) - The three situations where you still want to hand-edit Cargo.toml instead of using the marker README's Contributing section is now a one-line pointer.
…DK API changes (#127) * docs(CLAUDE.md): document required surfaces for MidenClient + React-SDK API changes Adds a 'Documenting public-API changes' section to the repo CLAUDE.md that maps every doc surface a contributor has to touch when changing the public API of either the MidenClient resource layer (web-client) or the React SDK. Replaces the vague 'update relevant per-package CLAUDE.md' bullet in the Contributing checklist with a precise mapping table. Surfaces enumerated for each side: TS types (api-types.d.ts), resource JSDoc, README narrative, per-package CLAUDE.md, root CHANGELOG, and the typedoc curation file when applicable. Includes triggers (when to touch each), conventions (terse tone, no speculative docs, cross-link PRs, one source of truth per fact), and a doc-only-PR escape hatch. Triggered by web-sdk#31, where the new transactions.batch API needed docs in five different places — easy to miss without an explicit map. * docs(CLAUDE.md): add the published-docs-site surfaces to the doc-process map The first pass missed three load-bearing surfaces of the public-docs pipeline: 1. The canonical user-facing site at https://docs.miden.xyz, hosted from 0xMiden/miden-docs, ingesting upstream repos via deploy-docs.yml's docs/external/src/* → docs/builder/<repo>/ copy step. 2. The typedoc-generated API reference at docs/typedoc/web-client/, which is regenerated from docs-entry.d.ts and verified by CI via git diff --exit-code (drift fails the build). 3. docs/external/src/ as the canonical narrative source the deploy-docs workflow ingests. After the web/WASM split this directory needs to exist in this repo (currently a gap — miden-client's docs/external/src no longer carries web/React content), and pages under it need to be added when shipping new public capabilities. Adds a 'Where the docs are published' table at the top of the doc-process section, plus a typedoc-regen workflow snippet, plus README ⇄ Docusaurus parity rule. Expands both the MidenClient and React SDK surface tables to call out docs/external/src/ and docs/typedoc/web-client/ explicitly, and updates the contributing-checklist item 3 to enumerate them. * fix(clippy): drop redundant & in export.rs format! arg Newer nightly clippy fires `useless_borrows_in_formatting` on the pre-existing &account_id.to_string() pattern. The lint started firing between origin/main's last green CI run (2026-04-30) and today — nightly clippy rolls forward unpinned in this repo's Makefile. Same fix as web-sdk#31's commit 7f9cf07. Including it here so the CLAUDE.md doc-process change isn't blocked by an unrelated baseline lint regression on the target branch. * fix(fmt): collapse format! to single line per nightly fmt After dropping the redundant & in commit 2b27e16, the call fits on one line and cargo +nightly fmt --check fails until we collapse the multi-line layout. Same pattern as the next-targeted PR (which already had it on one line).
The first pass of the doc-process section (#126/#127) prescribed 'regenerate typedoc and commit the diff' for any change to the curated public surface. That guidance was wrong: the in-repo 'Check that web client documentation is up-to-date' CI step runs typedoc and then a git diff --exit-code over the regenerated tree, but the directory hasn't been tracked since the web/WASM split — so the diff is empty and the step is a warning-only smoke test, not a gate. typedoc output is build artifact, not source. Updates four spots in the doc-process section: - 'Typedoc — keep in sync with the public API surface' → 'Typedoc — regenerated by CI, don't commit' - The MidenClient surface table row for docs/typedoc/web-client/ now reads 'Don't commit. Regenerated by CI...' - The Conventions bullet 'Regenerate typedoc' became 'Don't commit typedoc' - Contributing checklist item 3 mirrors that Triggered by web-sdk#31 audit feedback: noticed the autogen tree shouldn't have been committed there either. Companion .gitignore addition lands separately on web-sdk#31.
…134) * feat(web-client): multi-threaded WASM proving via wasm-bindgen-rayon Enables real parallel STARK proving in the browser by wiring wasm-bindgen-rayon through the SDK's WASM worker. Empirical: mint 22.9s -> 11.8s, consume 11.2s -> 5.1s, send 11.2s -> 6.5s on testnet (10 hardware threads, ECDSA accounts, fast build profile). All proofs verified by the testnet node and committed to chain. Toolchain + linker (.cargo/config.toml, rust-toolchain.toml): - nightly required for cfg(target_feature = "atomics") and -Z build-std - target-feature=+atomics,+bulk-memory,+mutable-globals applied at the [target.wasm32-unknown-unknown] level so it reaches every crate (including wasm-bindgen-rayon's compile_error guard, which RUSTFLAGS-via-env shadowed) - linker flags --shared-memory --import-memory so the WASM module imports a shared memory the host can clone into spawned workers; --max-memory=4GiB - explicit --export of __wasm_init_tls / __tls_size / __tls_align / __tls_base, otherwise lld GC's them and wasm-bindgen-cli's threading-prep pass fails ("failed to find __wasm_init_tls") Cargo (crates/web-client/Cargo.toml): - direct dep on miden-crypto with feature "concurrent" so cargo's feature unification turns on real rayon (dep:rayon) AND p3-maybe-rayon/parallel for the indirect deps that miden-client -> miden-tx -> miden-crypto pulls in. miden-crypto/concurrent is std-clean (unlike miden-tx/concurrent which transitively requires tempfile/tokio that don't compile to wasm32) - direct dep on rayon for diagnostic functions (rayonThreadCount, parallel/sequential SumBench) - wasm-bindgen-rayon = "1.3" Build pipeline (crates/web-client/rollup.config.js, package.json): - -Z build-std=std,panic_abort added to baseCargoArgs so std is recompiled with atomics enabled; without this, target-feature=+atomics emits a wasm with the bit set but cfg(target_feature = "atomics") still returns false in std code, breaking wasm-bindgen-rayon - target-feature flags moved out of --config build.rustflags (which RUSTFLAGS env shadows) and into .cargo/config.toml's [target.wasm32-unknown-unknown].rustflags (uniform application) - new wasmBindgenRayonSnippetResolver: rollup plugin (order: pre) that maps wasm-bindgen-rayon's workerHelpers.js import('../../..') to the wasm-bindgen output's index.js (rollup-plugin-rust internal resolveId would otherwise point '../../..' at a directory and fail EISDIR) - new emitWorkerHelpers factory: writes a sibling workerHelpers.js next to Cargo-*.js in dist/ and dist/workers/ with the start_worker shim (postMessage protocol from wasm-bindgen-rayon, but using __wbg_init named export instead of pkg.default since rollup-plugin-rust doesn't synthesize a default in bundler mode) - [remove-wasm-tla] plugin's "already exported?" guard was a false-positive on `default: __wbg_init,` inside the wasm-bindgen frozen-namespace object, so the export-rewrite was being skipped. Detect the actual top-level `export { ... }` token list instead JS init (crates/web-client/js/{constants.js, index.js, workers/web-client-methods-worker.js}): - rayon's global thread pool is per-WASM-instance. The SDK's web-client- methods-worker has its OWN WASM instance separate from the main thread, and every transaction's prove call runs there. initThreadPool(n) on the main thread alone sees rayon::current_num_threads() == 1 inside the worker, so par_iter()/par_chunks() in miden-crypto + p3-maybe-rayon fall through to sequential code despite the parallel features being on. Fix: pass numThreads in the SDK worker's INIT message, call wasm.initThreadPool(numThreads) inside the worker before the WebClient is constructed. numThreads defaults to navigator.hardwareConcurrency when crossOriginIsolated, else 1. Rust source (crates/web-client/src/lib.rs): - pub use wasm_bindgen_rayon::init_thread_pool (re-exported as initThreadPool) for callers that want to manage pool init themselves - rayonThreadCount() / parallelSumBench(n) / sequentialSumBench(n) diagnostic exports for verifying the pool is real (synthetic 4.4x speedup on FP-mix workload, matching real prove's 1.7-2.1x given parallel-fraction + Amdahl) Caveats and known issues: - This is the fast-build dev profile (MIDEN_FAST_BUILD=true, MIDEN_WEB_DEV=true). Release-build numbers will be different. - Page hosting the SDK MUST be cross-origin isolated (COOP: same-origin, COEP: require-corp) for SharedArrayBuffer. crossOriginIsolated === true is the gate; if false, the worker takes the numThreads=1 path and proves sequentially. - Diagnostic exports (rayonThreadCount, parallelSumBench, sequentialSumBench) should likely be feature-gated before this lands on main. * feat(client): MidenClient._getInnerWebClient escape hatch Public-but-@internal accessor returning the proxied JS WebClient that backs MidenClient. The proxy forwards missing properties to the wasm-bindgen WebClient, so callers can reach lower-level methods like executeTransaction, proveTransaction[WithProver], submitProvenTransaction, applyTransaction, newSendTransactionRequest, newConsumeTransactionRequest. Intended for advanced consumers that need to split the bundled execute → prove → submit → apply pipeline across contexts. The 0xMiden/wallet branch wiktor/mt-wasm-offscreen uses it to run prove in a chrome.offscreen document (where wasm-bindgen-rayon can spawn a thread pool) while keeping execute + submit + apply in the wallet's MV3 service worker. Marked @internal. The shape of #inner is intentionally not part of the documented public API and may change between SDK versions. Consumers depending on this should pin the SDK version. If a use case becomes common enough to warrant a stable public API, file an issue. * docs(build): record SIMD-target-feature attempt + regression Tried `target-feature=+simd128` on top of the LTO+wasm-opt release build, plus `--enable-simd` for binaryen. Measured a 16-37% prove-time REGRESSION across mint / consume / send on M-series Mac in V8/Chrome: | op | non-SIMD | +simd128 | delta | |---------|----------|----------|-------| | mint | 6.3 s | 8.6 s | +37% | | consume | 4.5 s | 5.2 s | +16% | | send | 3.5 s | 4.3 s | +23% | Root cause: Plonky3's Goldilocks backend has hand-tuned SIMD only for native x86_64 (AVX2 / AVX-512 IFMA) and aarch64 (NEON). For wasm32-unknown-unknown it falls back to scalar code, and LLVM's auto-vectorizer for `+simd128` on u64 modular arithmetic produces v128 sequences that lose to scalar on this hardware: - WASM v128 has no widening multiply (i64 × i64 → i128), so Goldilocks reduction emulates the high-half via shift+add chains that cost more than the scalar `mul` they replace. - The branchy modular-reduction step doesn't auto-vec cleanly. - LLVM tags too many short loops as "vectorize me" when setup cost outweighs throughput. - V8's WASM-SIMD-to-NEON lowering on M-series isn't as polished as its scalar lowering for this workload shape. Reverted at the config level. Documented the negative result in `.cargo/config.toml` so the next person who looks doesn't repeat the experiment without the context. Re-enabling `+simd128` should be paired with explicit `std::arch::wasm32` intrinsics in Plonky3's Goldilocks backend (upstream Plonky3 contribution), not just a target-feature flag. Also dropped `--enable-simd` from wasm-opt args (no longer needed without v128 in the input). `--enable-threads` stays (still required for the shared-memory atomics that wasm-bindgen-rayon depends on). Reference numbers confirmed unchanged: bench mint 6.3 s bench consume 4.5 s bench send 3.5 s WASM artifact size unchanged: 15.8 MB on-disk / 4.07 MB gzipped. * docs(changelog): add 0.14.4 entries for mt-wasm proving + _getInnerWebClient escape hatch * fix(ci): toml format, clippy cast_precision_loss in bench helpers, build-wasm target uses nightly + build-std - taplo fmt: rustflags table in .cargo/config.toml needed one-arg-per-line format; same for Cargo.toml's [package.metadata.docs.rs] entry. - clippy: add #[allow(clippy::cast_precision_loss)] to parallelSumBench / sequentialSumBench. These are diagnostic helpers exercising rayon dispatch with a synthetic FP-mix workload — precision loss is intentional, the work just needs to defeat constant-folding. - Makefile build-wasm: stable cargo trips at link with .cargo/config.toml's +atomics target-feature because rust-std-wasm32 from rustup is built WITHOUT atomics. Switch to nightly + -Z build-std=std,panic_abort to match what the rollup build (baseCargoArgs in rollup.config.js) already uses. Removes the silent divergence where CI's build-wasm was producing a different artifact than the published one. * feat(build): dual-build ST + MT WASM, /mt subpaths for opt-in multi-threading The previous mt-wasm work shipped a single multi-threaded WASM that required cross-origin isolation just to LOAD — even consumers using delegated proving exclusively. That was a breaking regression for non-COI contexts. This restores the v0.14.2 default behavior (single- threaded WASM that loads anywhere) while keeping multi-threaded as an opt-in. Subpaths: '@miden-sdk/miden-sdk' → eager-st (default; safe everywhere) '@miden-sdk/miden-sdk/lazy' → lazy-st '@miden-sdk/miden-sdk/mt' → eager-mt (opt-in; requires COI) '@miden-sdk/miden-sdk/mt/lazy' → lazy-mt Mechanics: - New 'mt-threads' cargo feature on miden-client-web gates the rayon + wasm-bindgen-rayon deps and miden-crypto/concurrent (which propagates to Plonky3 via cargo feature unification, enabling the parallel prover paths). - '+atomics', shared-memory + import-memory linker flags, TLS exports, and the -Z build-std=std,panic_abort flag all moved out of .cargo/config.toml into per-invocation cargo args in the rollup config. ST build uses stable cargo with the precompiled rust-std-wasm32 (no atomics, no build-std, no nightly). MT build uses nightly with all the previous flags. - Rollup config is now driven by MIDEN_BUILD_VARIANT={st,mt}, output to dist/{st,mt}/. package.json's 'build' runs both sequentially. MIDEN_FAST_BUILD=true (PR CI) skips the MT step via a small conditional script, keeping PR CI ~15 min on the ST-only path. - Makefile gains 'build-wasm-mt' alongside 'build-wasm' (now ST only). Verified locally: both variants produce working WASM artifacts. ST glue contains no 'shared:true' references; MT glue does. Total wall-clock ~5 min for both builds with MIDEN_FAST_BUILD=true on warm caches. * fix(build): keep -Z build-std for ST too, rewrite worker wasm.js import per variant CI failures from the dual-build push: 1. Build Client for Wasm / Build Web Client / publint+attw all hit 'can't find crate for std'. Cause: my removal of -Z build-std=std,panic_abort from the ST cargo args meant ST needed precompiled rust-std-wasm32, but CI doesn't install that for the auto-installed nightly toolchain (it installs rust-src, which build-std uses). Fix: keep -Z build-std on the ST path too. Costs ~30s per ST build but avoids a CI workflow change. Stable + precompiled std for ST is a follow-up if we ever need it. 2. Worker bundle couldn't resolve '../../dist/wasm.js' — the static path was correct pre-dual-build, but post-change the file is at dist/{st,mt}/wasm.js. Added a small rollup resolveId plugin (rewriteWorkerWasmImport) that rewrites the import to point at the variant-specific output. Applied to both classic + module worker configs. 3. taplo fmt run on Cargo.toml (single-line array reflow). 4. Knip: clean.js shows as unused because my new build-types.js exec's it indirectly. Added clean.js to web-client entry list. Cpr added to ignoreDependencies (also exec'd by build-types.js). Removed RUSTUP_TOOLCHAIN=stable overrides from the ST package.json script and Makefile target — same root cause (CI doesn't install stable + wasm32 target), and forcing nightly via rust-toolchain.toml works fine for both variants now. * feat(react-sdk): mt + mt/lazy subpaths; document COI requirement; drop cpr React SDK (packages/react-sdk): - Two new exports parallel to the existing /lazy split: '/mt' (eager + MT) and '/mt/lazy' (lazy + MT). Same source tree; tsup writes four bundles with file-level rewrites of '@miden-sdk/miden-sdk/lazy' to the matching SDK subpath ('', '/lazy', '/mt', '/mt/lazy'). Node-10 fallback shims added at packages/react-sdk/mt/{,lazy/}package.json mirroring the existing lazy/ shim. Web SDK README (crates/web-client/README.md): - 'Entry Points' section reworked to explain the eager × ST/MT 4-cell matrix. Added 'Setting cross-origin isolation headers' subsection with per-host snippets (Vite, Next.js, Express, Chrome/Firefox MV3 manifest). Documented the COEP side-effect (cross-origin resources need CORP/CORS) and the COI service-worker shim as an option for consumers who can't set headers. Documented 'initThreadPool(n)' as a required one-time call before any MT prove. React SDK README (packages/react-sdk/README.md): - New 'Subpaths: Eager / Lazy × ST / MT' section replacing the old Next.js/SSR-only intro. Added 'Multi-threaded proving (/mt, /mt/lazy)' subsection with the bring-up-the-pool example using a small ThreadPoolBoot component inside MidenProvider. Tooling: - Dropped 'cpr' devDependency from crates/web-client/package.json. The one consumer (scripts/build-types.js) now uses fs.cpSync directly, which works regardless of how the script is invoked (PATH-independent). - Knip ignoreDependencies entry for cpr removed too. * chore(deps): refresh pnpm-lock.yaml after dropping cpr from web-client * fix(build): scripts + typedoc + playwright now point at dist/st/ post-dual-build After the dual-build restructure, dist/ has no top-level files — everything's under dist/{st,mt}/. Several CI-only paths still hardcoded the old layout: - crates/web-client/typedoc.json: entry was './dist/docs-entry.d.ts', now './dist/st/docs-entry.d.ts'. - crates/web-client/scripts/check-{bindgen-types,method-classification, standalone-types}.js: hardcoded dist/crates/miden_client_web.d.ts paths, now dist/st/crates/... - crates/web-client/playwright.config.ts: webServer was 'npx http-server ./dist', now serves './dist/st' so integration tests' implicit page.evaluate(() => import('./index.js')) resolves correctly. Type declarations are identical between the ST and MT variants (the WASM impl is feature-gated but the .d.ts is uniform), so checking just the ST output is sufficient for these scripts. Integration tests run against ST too — the MT variant needs cross-origin isolation headers on the dev server, separate change if/when we want to exercise it in CI. * fix(build): always build both ST + MT variants (publint requires advertised dist files to exist) The MIDEN_FAST_BUILD-skips-MT pattern saved ~3-5 min in PR CI but broke publint, which reads package.json's exports field and verifies every advertised subpath target file is in the packed tarball. With MT skipped: pkg.exports["./mt"].import.types is ./dist/mt/index.d.ts but the file does not exist. Three options considered: a) Drop /mt + /mt/lazy from exports (defeats the dual-build purpose). b) Force MT build only inside check-publish (runs publint outside fast mode, ~5 min slower for that one job). c) Always build both (~3-5 min slower across every PR-CI job that runs the SDK build). Going with (c): the invariant 'pnpm run build always produces every variant the package.json claims' is the simplest mental model. PR CI's ~5 min increase is acceptable; the integration test shards (each 12-18 min) dominate runtime anyway. Removes scripts/build-mt-conditional.js since its sole purpose was to honor the now-defunct skip path. * fix(docs): tsconfig.docs.json files now point at dist/st/ post-dual-build Pairs with the earlier typedoc.json + check-bindgen-types.js + check- standalone-types.js + check-method-classification.js fixes — last leftover hardcoded dist/ path. Type declarations are identical between ST and MT variants, so checking only ST is correct. * fix(build): patch out rollup-plugin-rust optimize_for_size std flag @wasm-tool/rollup-plugin-rust 3.1.x unconditionally appends `-Z build-std-features=optimize_for_size` to the cargo invocation when building in release+rustc-optimize mode. That causes std to be recompiled with size-priority paths (simpler sort, smaller memcpy, weaker HashMap hash) — the exact opposite of what a STARK prover needs, since hot std paths in miden-crypto / p3-maybe-rayon dominate the prove time. Empirical regression: ECDSA-MT send goes from ~3.5s (without the flag) to ~12s (with it). ST is similarly degraded. Affects every release-profile build the plugin produces; PR-CI runs with MIDEN_FAST_BUILD=true so they skip wasm-opt + LTO and don't trip this, but the published artifact would. Fix via pnpm.patchedDependencies — patch-package equivalent for pnpm. Auto-applied on every install. No-op on clean upstream installs after the line is removed in a future plugin release. Repro: a separate Next.js bench (https://github.com/WiktorStarczewski/miden-prover-bench) exercises 10× send→consume cycles and reports avg/median/min/max prove times per phase. Patch confirmed by direct A/B. * perf(mt): enable +simd128 with LLVM auto-vectorization disabled The earlier blanket attempt at `+simd128` regressed prove time 16-37% (commit 05dcac9). Root cause was LLVM auto-vectorization of Goldilocks u64 modular reduction: WASM v128 has no widening 64×64 multiply, so LLVM emitted shift+add chains that cost more than the scalar `mul` they replaced. Re-enable `+simd128` paired with: -C llvm-args=-vectorize-loops=false -C llvm-args=-vectorize-slp=false This suppresses LLVM auto-vectorization globally while still letting hand-written WASM-SIMD paths in libraries activate via `cfg(target_feature = "simd128")` (BLAKE3 etc). Goldilocks scalar code stays scalar. Also adds `--enable-simd` to wasm-opt args so binaryen accepts v128 instructions in the input module. Measured on testnet, ECDSA, M-series Mac, 10-cycle send/consume bench in proving-bench-nextjs: median send: 4173 ms -> 3895 ms (-6.7%) median consume: 4132 ms -> 3903 ms (-5.5%) min send: 3795 ms -> 3325 ms (-12%) min consume: 3799 ms -> 3499 ms (-7.9%) WASM size: 17.4 MB -> 16.2 MB (about 7% smaller — autovec was inflating binary size with mis-vectorized v128 codegen). Variance widened slightly (max-cycle outliers got worse), but typical- case prove time is faster and best case is meaningfully faster. Net positive for end users. The deeper win remains upstream: hand-written std::arch::wasm32 intrinsics for Goldilocks mul/reduce in p3-goldilocks (Plonky3). That PR would close the WASM-vs-native gap meaningfully — this flag combo is a free incremental on top once it lands. * review: bump to 0.14.6, pin nightly, replace _getInnerWebClient with _withInnerWebClient Addresses review feedback on PR #134: 1. Bump @miden-sdk/miden-sdk and @miden-sdk/react to 0.14.6. The branch added meaningful surface (MT-threaded WASM build, dual ST/MT subpaths, optimize_for_size patch, simd128 + autovec disabled, the inner-client escape hatch); the next published artifact should reflect that with a version bump. 2. Pin rust-toolchain.toml to nightly-2026-05-05 (was floating `nightly`). Floating nightly silently rebuilds against whatever's current on each build, which (a) shifts the published WASM bytes between releases and (b) breaks PGO compatibility (profile data is keyed to LLVM version). Bump deliberately, never via floating channel. 3. Replace synchronous getter `_getInnerWebClient()` with callback-based `_withInnerWebClient(fn)` that runs `fn` inside `_serializeWasmCall`. The previous getter returned the proxied client and let the caller bypass the SDK's WASM-call serialization chain — concurrent access would trip wasm-bindgen's "recursive use of an object detected" RefCell panic. The new shape holds the lock for the duration of `fn` so downstream consumers don't have to bring their own mutex (the wallet already does, but external consumers shouldn't have to). 4. Verified `--features mt-threads` actually enables the parallel chain. `cargo tree -e features` is misleading here (only shows direct feature dependencies, hides feature unification). `cargo build -v` is ground truth: with mt-threads, rustc compiles for wasm32-unknown-unknown with --cfg 'feature="concurrent"' on miden-crypto AND --cfg 'feature="parallel"' --cfg 'feature="rayon"' on p3-maybe-rayon AND --cfg 'feature="parallel"' on p3-util and p3-miden-lifted-stark. Full chain unifies. (No code change for this one — verification only.) CHANGELOG entry promoted from 0.14.4 (TBA) → 0.14.6 (TBA) and rewritten to document the perf + build changes (simd128, optimize_for_size patch, toolchain pin) alongside the original MT and escape-hatch features. * docs(changelog): drop build/perf entries — internal toolchain changes don't belong in user-facing changelog * fix(rollup): typos check rejects 'mis-vectorizing' — reword to 'incorrectly vectorizing' * fix(react-sdk): bump peer + wallet example dep to ^0.14.6 to match web-client * build(rollup): keep ST path on stable Rust, MT stays nightly Decouples the two build paths' toolchain requirements. The MT path needs nightly for cfg(target_feature = "atomics") to flip true (stable 1.93 silently emits +atomics but the cfg() check still says false, breaking wasm-bindgen-rayon's compile-time gate) and for -Z build-std to recompile std with atomics enabled. Nothing in the ST dep graph requires nightly once mt-threads gates wasm-bindgen-rayon / rayon / concurrent out, so flipping ST to stable widens trust window for downstream consumers pinning their own toolchains and avoids breakage when nightly regresses. Changes: - crates/web-client/rollup.config.js: set RUSTUP_TOOLCHAIN=stable for the ST variant only (rustup precedence: env > rust-toolchain.toml). Move -Z build-std=std,panic_abort from baseCargoArgs into mtOnlyCargoArgs. - CI workflows: install stable wasm32-unknown-unknown std alongside the pinned nightly so the ST build can link without -Z build-std. Validated locally: rollup ST build produces a 14.6MB wasm in 4m58s on stable, no errors. MT path unchanged. Addresses review feedback on #134 from @SantiagoPittella. * mt-threads: propagate `concurrent` through miden-client Per Santiago's review thread (PR #134 r3196048839): the previous `miden-crypto/concurrent` only enabled rayon parallelism in miden-crypto (Merkle/hash primitives). The prover-side parallelism in miden-tx + miden-prover stayed single-threaded, so the published MT WASM bundle was shipping with only a partial multi-thread win. Switch to `miden-client/concurrent`, which cascades: miden-client/concurrent -> miden-tx/concurrent -> miden-prover/concurrent -> miden-crypto/concurrent (transitively) Verified with: cargo tree -p miden-client-web --features mt-threads \ --target wasm32-unknown-unknown -f "{p} {f}" Before: only miden-crypto had `concurrent`. After: miden-tx, miden-prover, and miden-crypto all have `concurrent`. The miden-client `concurrent` feature lands in 0xMiden/miden-client PR #2169. Until that publishes to crates.io, the workspace pulls miden-client via a temporary `[patch.crates-io]` git ref pointing at the PR's branch — drop the patch and bump the workspace miden-client version once the PR ships. * mt-threads: drop committed [patch.crates-io] (use Client PR marker instead) The previous commit erroneously committed a `[patch.crates-io]` block pinning miden-client to the PR branch — this repo has an established convention for exactly that scenario (web-sdk CLAUDE.md "Linking a web-sdk PR to an in-flight miden-client PR"). Setting `Client PR: 0xMiden/rust-sdk#2169` in the PR description makes the inject-linked-client-pr CI action apply the same patch runner-locally without polluting the committed tree. The pre-commit no-linked-client-pr-block hook caught my version mid-flight here; thanks hook. * address review: gate diagnostics behind testing; tidy comments * chore: bump workspace + npm packages to 0.14.8; consume published miden-client 0.14.8 * fix(ci): pin linked client PR by rev (sha) instead of branch, survive post-merge auto-delete * chore: bump example wallet SDK deps to 0.14.8 --------- Co-authored-by: Wiktor Starczewski <wiktor.s@miden.team>
…ker opt-out (#149) * feat(rust): miden-mobile-prover crate with C ABI for iOS/Android FFI * feat(web-client): add ClientOptions.useWorker to opt out of Web Worker shim The WebClient shim wraps every WASM call in a Web Worker for UI responsiveness, but the worker boundary serializes the prover via TransactionProver.serialize() — a format with no encoding for newCallbackProver(jsFn). A CallbackProver is silently downgraded to 'local' and the worker spawns its own in-process WASM prover, bypassing the caller's JS callback. This blocks mobile/Tauri/Capacitor consumers that want to route prove through a native plugin (e.g. miden-mobile-prover on iOS/Android). Add useWorker?: boolean to ClientOptions (default true; no behavior change for existing consumers). When false, the WebClient skips the worker and dispatches WASM calls directly on the current thread, so the prover handle reaches wasmWebClient.proveTransactionWithProver with the closure intact. Wired through: - crates/web-client/js/index.js: WebClient constructor + both factories - crates/web-client/js/client.js: MidenClient.create plumbing - crates/web-client/js/types/api-types.d.ts: ClientOptions type - packages/react-sdk: MidenConfig.useWorker → MidenProvider's WebClient.createClient[WithExternalKeystore] * fixup! feat(web-client): add ClientOptions.useWorker to opt out of Web Worker shim * feat(web-client): add TransactionProver.newCallbackProver(jsFn) Companion to the useWorker opt-out: provides the JS-callable prover variant that consumers can pass to client.transactions.{send,consume,submit} to route prove() through a JS function. The variant wraps the JS Function in a Send+Sync impl of TransactionProverTrait whose prove() dispatches to the closure and awaits its Promise<Uint8Array>. Required for mobile native-prover plug-in flows (iOS/Android Capacitor + miden-mobile-prover) — the wallet hands the SDK a CallbackProver whose closure base64-encodes the inputs, calls MidenNativeProver.prove via Capacitor, and decodes the result back to Uint8Array. Combined with ClientOptions.useWorker:false, the closure survives the WebClient dispatch path and reaches the wasm-bindgen binding intact. * chore(web-client): drop debug console.log from JsCallbackTransactionProver The 6 console_log calls inside prove() were left over from debugging the bridge end-to-end. They fired on every local prove and produced noise in consumer devtools without conveying anything useful that error returns don't already cover. The console_log extern is also removed (had no other callers). * docs(changelog): newCallbackProver + useWorker entries under 0.14.6 (TBA) * chore(ci+docs): publish miden-mobile-prover, fix changelog version, doc Client-PR marker - publish-crates-release.yml now publishes the miden-mobile-prover crate on release (preflight dry-run, then publish), alongside idxdb-store and web-client. - CHANGELOG.md: move newCallbackProver + useWorker entries from the stale '0.14.6 (TBA)' heading (0.14.6 shipped 2026-05-05) into a fresh '0.14.9 (TBA)' section. 0.14.7 and 0.14.8 are also published. - CLAUDE.md (Linked-PR section + CHANGELOG section): spell out that the 'Client PR: #N' marker is the load-bearing handle for cross-repo CI and that prose mentions don't trigger the linked-PR pipeline; spell out that CHANGELOG entries must go under a version strictly higher than the latest published release (the '(TBA)' heading at the top of the file lags releases and isn't trustworthy on its own). * chore(release): bump to 0.14.9 - Cargo workspace 0.14.8 → 0.14.9 (cascades to miden-client-web, miden-idxdb-store, miden-mobile-prover via workspace inheritance). - npm packages 0.14.8 → 0.14.9: @miden-sdk/miden-sdk, @miden-sdk/react, @miden-sdk/vite-plugin. - @miden-sdk/react's peer dep on @miden-sdk/miden-sdk bumped to ^0.14.9. - Cargo.lock refreshed accordingly. * chore(lint): fix format + clippy doc_markdown + toml-fmt on provers/mobile-prover * chore(release): bump wallet example deps to 0.14.9
…nt trigger (#163) * fix(ci): stop linked-client-pr gate from spamming failures via status cap The "Linked client PR ready" gate re-runs every 15 min on a cron and POSTed a fresh `linked-client-pr-ready` commit status on every run, even when the verdict was unchanged. GitHub caps each (commit SHA, context) pair at 1000 statuses, so any PR whose head SHA sat unpushed eventually hit the cap; the POST then returned 422 and the job failed, emailing the PR author every 15 minutes. Two changes to set_status: 1. Idempotency guard: read the latest existing `linked-client-pr-ready` status for the head SHA and skip the POST when state + description are unchanged. This stops the email spam and stops accumulating statuses. 2. Tolerate the 422 "maximum number of statuses" response as non-fatal (the verdict is unchanged and re-POSTing is impossible anyway) for SHAs that already exhausted the cap, while still surfacing any other failure such as a 403 from a missing `statuses: write` scope. Also routes the no-marker success path through set_status so it benefits from the same guards; target_url is now attached only when a linked PR is known. * fix(ci): run linked-client-pr gate on pull_request_target events The workflow triggers on `pull_request_target`, but the `gate` job's `if:` guarded on `github.event_name == 'pull_request'` — a value that never occurs under this trigger. The PR-triggered path was therefore dead: the gate only ever ran via the schedule cron, never when a PR was opened, edited, or synchronized. That defeats the reason the workflow uses `pull_request_target` (so it can post a status on fork PRs). Guard on `pull_request_target` instead so the gate evaluates and posts `linked-client-pr-ready` on PR events as intended. The matrix's non-schedule branch already reads `github.event.pull_request.number`, which is populated on `pull_request_target`, so it needs no change.
…152) * fix(web): _withInnerWebClient re-entrancy — depth-tracked inline run * chore(release): bump web-sdk to 0.14.10 * chore(release): bump wallet example deps to 0.14.10 * review(152): add re-entrancy test to ci-shard-4 so CI picks it up @SantiagoPittella's review noted the new `with_inner_web_client_reentrancy.test.ts` wasn't being executed by CI. Confirmed: the file is at the right path and the default chromium project's `testMatch: "*.test.ts"` matches it locally, but CI runs via the manually-balanced `ci-shard-N` projects, which only execute files explicitly listed in their `testMatch` arrays. Added the file to shard 4 ("compile-and-misc"), which is the natural home for an infrastructure/API regression test (alongside the similarly protocol-flavoured `miden_client_api.test.ts` and `settings.test.ts`). * fix(test): drive reentrancy test through MidenClient wrapper The re-entrancy regression test called `client._withInnerWebClient(...)` on `window.client`, but the test harness sets `window.client` to the proxy-wrapped inner WebClient (created via `WasmWebClient.createClient`), which owns `_serializeWasmCall` but not `_withInnerWebClient` — `_withInnerWebClient` is a method on the `MidenClient` wrapper. Every test failed with `client._withInnerWebClient is not a function`. Wrap the existing inner client in a real `MidenClient` (exported from index.js, attached to `window` by the harness) and drive `_withInnerWebClient` through it. External `syncState` calls move to the shared inner client so they still queue on the same `_serializeWasmCall` chain the inner slot holds — the wrapper exposes `sync()`, not `syncState()`. Touches only this test; the other shard-4 tests keep using `window.client` as the raw inner client. * test: rewrite reentrancy test 3 as a characterization test Test 3 asserted that an external SDK call made WHILE `fn` is in flight queues behind the `_withInnerWebClient` outer slot. The fix's design makes that impossible on the same client: `_withInnerLockDepth` is a global counter on the inner client, so while `fn` awaits (depth > 0) any `_serializeWasmCall` — including one from code outside `fn` — runs inline, not queued. The PR's own SAFETY CONTRACT documents this and requires callers to hold an external mutex to avoid it. The test was asserting a guarantee the design intentionally does not make. Rewrite it to pin the documented behavior instead: an external call during `fn` runs inline, so "external-ran" precedes "inner-end". Use a cheap proxy-fallback read (local store lookup) rather than a networked `syncState` for the external call so the ordering is deterministic and not subject to node latency. Drop the now-unused `innerFinished` deferred. If a future change closes the hole (context-scoped depth) or reintroduces the deadlock, this assertion flips and flags the change. * test: make reentrancy test 3 deterministic via explicit handshakes The previous version asserted a natural-timing ordering between the external call and `fn`'s completion. That ordering is a race: `_serializeWasmCall`'s inline path is `Promise.resolve().then(fn)`, so whether an external call runs inline vs queued depends on whether `_withInnerLockDepth > 0` at the exact microtask it fires — two CI runs produced opposite orderings (external-ran before vs after inner-end). Replace the timer race with explicit handshakes: `fnEntered` ensures the external call is issued only once `fn` is mid-flight (depth = 1, forcing the inline path), and `fn` parks on `externalDone` until the external call returns, so its inline execution is observable before the outer slot resolves. A 5s `Promise.race` converts the regression case (call queues behind the parked outer slot → deadlock) into a fast, explicit "external-blocked" failure instead of a hang. Prettier-clean.
… / JsStorageSlot / JsVaultAsset (#150) * fix(web): drop inspectable from JsAccountUpdate / JsStorageMapEntry / JsStorageSlot / JsVaultAsset Resolves miden-client#2183: under Next.js 16.2 dev-mode the patched console.* path (clientFileLogger.log) runs every non-primitive argument through safe-stable-stringify, which invokes toJSON() automatically. These four wasm-bindgen structs had #[wasm_bindgen(getter_with_clone, inspectable)], which caused wasm-bindgen to auto-emit a toJSON() reading every public field via wasm.__wbg_get_<class>_<field>(this.__wbg_ptr) — i.e. each console.log(update) fired 11 WASM getter calls. If the underlying pointer had been freed or another WASM call was in flight, the resulting 'null pointer passed to rust' trap propagated out of the user's console.log call site and crashed the caller. Other inspectable usages in the codebase (Address, NoteFile, CodeBuilder, miden_array) wrap inner objects as tuple structs with no public fields, so wasm-bindgen's auto-toJSON for them is a no-op — they were never affected and stay as-is. Dropping the attribute makes the JS class behave like the 15 other wasm-bindgen exports the issue references: no toJSON() method, so JSON.stringify / safe-stable-stringify see an empty wrapper and return {}. Field access via the named getters is unchanged — only the auto-stringification path is muted. CHANGELOG entry added under a new 0.14.10 (TBA) section (0.14.9 was just shipped — per the project rule against adding to released sections). * test(web): regression test for miden-client#2183 Locks in the contract enforced by the previous commit: the wasm-bindgen wrapper classes for JsAccountUpdate, JsStorageMapEntry, JsStorageSlot, and JsVaultAsset must NOT expose a toJSON() method. Without toJSON(), safe-stable-stringify (used by Next.js 16.2 dev-mode console.* patching) falls back to {} on these wrappers and the __wbg_get_<class>_<field> re-entry path can never fire. The test runs in the existing crates/web-client Playwright harness; it loads the built SDK on window and asserts both (a) prototype.toJSON is undefined for all four classes, and (b) JSON.stringify of a fabricated JsAccountUpdate instance returns '{}'. If anyone re-adds inspectable to these structs (or otherwise emits an auto-toJSON), the test fails immediately. * test(web): broaden regression test to every wasm-bindgen wrapper The original idxdb_store_no_tojson.test.ts only checked the four classes named in miden-client#2183 (JsAccountUpdate, JsStorageMapEntry, JsStorageSlot, JsVaultAsset). The actual failure mode — a wasm-bindgen-auto-generated toJSON that reads fields via `__wbg_get_<class>_<field>(this.__wbg_ptr)` — can happen to ANY struct declared `#[wasm_bindgen(inspectable)]` with `pub` fields, not just those four. Rename to no_wasm_reentry_via_tojson.test.ts and widen the contract: enumerate every export on `window` that looks like a wasm-bindgen wrapper (static `__wrap`, prototype `__destroy_into_raw`), fabricate an instance with `__wbg_ptr = 0`, and assert JSON.stringify returns '{}'. That property holds for: - classes with no toJSON (default JSON.stringify behavior), - classes with wasm-bindgen's safe `toJSON() { return {}; }` shape (emitted for `inspectable` structs that have no `pub` fields — Address, NoteFile, CodeBuilder, the array wrappers, etc.), - any class with an explicit toJSON that doesn't read WASM fields. It fails only for the dangerous shape: toJSON reading fields through WASM getters. Anyone who later adds `inspectable` to a struct with `pub` fields will see this assertion fire with the offending class name in the failure message, regardless of whether the new struct is in idxdb-store or anywhere else.
Brings the 43 main-only commits (0.14.6–0.14.11) onto next, keeping next's 0.15 API surface. Key features restored on next: - useWorker opt-out (ClientOptions + MidenConfig) [#149] - TransactionProver.newCallbackProver + JsCallbackTransactionProver [#149] - crates/mobile-prover (Cargo deps still 0.14 — ported to the 0.15 workspace deps in a follow-up commit on this branch) [#149] - _withInnerWebClient + depth-tracked re-entrancy fix [#152] - Multi-threaded WASM proving: ST/MT dual build, dist/st + dist/mt layout, /mt + /mt/lazy subpaths, nightly toolchain pin [#134] - inspectable dropped from JsAccountUpdate/JsStorageMapEntry/ JsStorageSlot/JsVaultAsset [#150] - react-sdk fixes: useConsume ownership [#138], TransactionId.toHex [#140] - linked-client-pr gate status-cap + dead-trigger fixes [#163] - docs-skip 'changes' job gating in test.yml Conflict-resolution policy: next's side for everything 0.15-shaped (version 0.15.0-alpha.4, git-pinned miden-client, napi/node build, js_export structure, MIDEN_CLIENT_REF pin); main's side for the MT build system and CI hardening; unions for CHANGELOG/CLAUDE/knip and the exports maps (node condition + dist/st layout + /mt subpaths). auto-release-main.yml deleted (main-branch-only workflow). Locks regenerated, not hand-merged.
- mobile-prover: depend on miden-client/concurrent (the 0.15 client exposes the feature, so the direct miden-tx/miden-protocol deps from the 0.14 workaround are gone); document that the TransactionInputs/ ProvenTransaction wire format is per-protocol-line and 0.14-built native binaries do not interoperate with a 0.15 SDK - provers: newCallbackProver + JsCallbackTransactionProver re-added as a browser-gated #[wasm_bindgen] impl (js_sys::Function has no napi representation, so the Node binding keeps local/remote provers only) - BlockHeader.feeFaucetId(): 0.15 spelling of main's nativeAssetId() (FeeParameters::native_asset_id was renamed fee_faucet_id upstream); needed by wallet native-asset discovery - lib.rs: compile_error! guard against mt-threads × nodejs; MT diagnostics additionally gated on the browser feature so the napi build with testing enabled keeps compiling - react tests: txId mocks expose toHex() to match real WASM behavior (the hooks now use main's #140 toHex fix; next's mocks still returned the hex from toString, which masked the original regression); useSend prover-branch test asserts on proveTransaction(result, prover) — the 0.15 call shape — instead of main's proveTransactionWithProver - CHANGELOG: 0.15.0 entries for the ported surface + feeFaucetId
A method-surface diff of client.js/index.js between main and the merged tree (next rewrote whole regions of these files for the 0.15 port, so git's hunk-level merge silently preferred next's side) surfaced three public APIs that exist on main but were missing after the merge: - MidenClient.ready() — WASM-init gate for /lazy consumers; the wallet's ensureSdkWasmReady() depends on it - MidenClient.waitForIdle() — drain the serialized WASM call chain so a host can clear in-memory auth keys without racing the kernel's auth callback - MidenClient.lastAuthError() — raw JsValue the sign callback last threw (typed reason/code convention); the wallet's auth-error classification (readLastAuthReason) depends on it. Full pipeline restored: the SignCallbackError carrier in web_keystore_callbacks.rs, the last_sign_error RefCell capture in web_keystore.rs, the browser-gated WebClient::last_auth_error getter (adapted from main's inner_keystore pattern to next's inner.borrow() cell), the SYNC_METHODS entry, the client.js delegate, and the api-types.d.ts declarations
…erences it on next)
…ght project The test asserts on idxdb-store's Js* classes and the wasm-bindgen WebClient copied onto window by the browser global setup — none of which exist under the napi binding. main had no nodejs project, so the merge made it run there for the first time.
The test predates the main/next split and guards both directions of the entry contract: dist/st/eager.js must have WASM initialized at import (sync constructors work immediately), and dist/st/index.js must NOT TLA-init WASM (sync constructor throws before getWasmOrThrow resolves). next's js_export/napi test-suite rewrite (#13) deleted it, leaving the package's default entry (main/browser → dist/st/eager.js) untested; the merge inherited that deletion and the knip ignoreUnresolved entry for ./eager.js went stale — restoring the test un-stales it. Excluded from the nodejs Playwright project (page fixture + browser-only dynamic imports). Verified green locally against the merged dist.
- ci: eager_entry.test.ts added to ci-shard-4's testMatch — main carried it in shard-4, the merge dropped the entry, and CI only invokes the shard projects, so the restored test was never executing in CI - react-sdk: the variant rewrite in tsup.config.ts keyed off the /lazy specifier, but next's source tree imports the bare @miden-sdk/miden-sdk — the rewrite never matched and every variant (/lazy, /mt, /mt/lazy) silently bundled the eager-ST SDK. The rewrite now keys off the exact quoted bare specifier and THROWS when nothing matches, so a future specifier change cannot silently ship wrong variants again. Verified: all four dist bundles emit their correct per-variant subpath - node: waitForIdle resolves immediately (the napi binding has no detached call chain to drain) and lastAuthError returns null (signing goes through FilesystemKeyStore — a JS sign callback can never run), so the cross-platform MidenClient type surface holds on Node instead of throwing TypeError - docs: lastAuthError docstrings state the useWorker:false requirement (the worker shim's keystore lives in the worker WASM instance, so the main-thread accessor reads null there) - test: with_inner_web_client_reentrancy first case comment matched main's serializing proxy; next's proxy binds fallback reads directly, so the case guards the borrow-check invariant, not chain re-entrancy
…roject Same class as no_wasm_reentry_via_tojson: the tests came from main (which had no nodejs Playwright project) and exercise the browser worker-shim chain mechanics — _serializeWasmCall, the _withInnerLockDepth counter, chain release on rejection. The napi client serializes in Rust and has no JS call chain, so all four cases TypeError'd under nodejs. They were masked in the previous run because no_wasm_reentry aborted the suite earlier.
WiktorStarczewski
added a commit
that referenced
this pull request
Jun 11, 2026
Bumps @miden-sdk/miden-sdk, @miden-sdk/react (+ peer), and the three node-sdk platform packages to 0.15.0-alpha.5. First alpha published from the unified branch (PR #177): restores useWorker, newCallbackProver, _withInnerWebClient, MidenClient.ready/waitForIdle/lastAuthError, BlockHeader.feeFaucetId, the miden-mobile-prover crate, and MT proving via the /mt + /mt/lazy subpaths on the 0.15 protocol surface. Release notes must flag: nightly toolchain required for the MT build; react-sdk is now esm-only; mobile-prover wire format requires a matching 0.15 SDK (0.14-built native binaries do not interoperate).
This was referenced Jun 11, 2026
WiktorStarczewski
added a commit
that referenced
this pull request
Jun 12, 2026
…t merge (#184) The #177 unification dropped the _serializeWasmCall chain from the direct (no-worker) path everywhere it existed on main: - createClientProxy bound fallback methods (getAccount, getAccounts, getTransactions, ...) raw to the WASM client instead of routing them through the chain (SYNC_METHODS stay raw-bound by design); - the five transaction methods and three sync methods called the raw client directly in their !this.worker branches. Every such call holds the WASM client's internal RefCell across its awaits, so two overlapping calls panic with 'RefCell already borrowed' and poison the instance — later calls throw 'Unreachable code should not be executed' and in-flight promises can stay pending forever. useWorker:false consumers (Capacitor/WKWebView, callback-prover hosts) hit this deterministically; miden-wallet's mobile build panicked at wallet creation when front-end balance polling overlapped the initial syncs (the mobile leg of #180). All direct-path calls now route through the chain exactly as on main and 0.14.x. The sync methods keep their coalescing lock as the outer lock (sync lock → chain, in that order, on every path — no deadlock). Covered by a burst-concurrency regression test that hangs/panics on the unfixed build and completes in ~3s on the fixed one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Merges
main(0.14.x release line, through 0.14.11) intonext(0.15 series), keeping next's 0.15 API surface while bringing over every main-only feature and fix — 43 commits. This unblocks the Miden Wallet's migration to 0.15: four wallet-critical APIs existed only onmainuntil now.Restored / ported onto 0.15
ClientOptions.useWorkeropt-out +MidenConfig.useWorkerworkerMode(off ⇒ no shim at all;workerModeonly picks classic/module when the shim is on)TransactionProver.newCallbackProver(jsFn)#[wasm_bindgen]block (js_sys::Functionhas no napi shape; Node binding keeps local/remote)crates/mobile-prover(C-ABI native prover)concurrentnow cascades frommiden-clientitself. Wire format note: 0.15 changedTransactionInputs/ProvenTransactionserialization — native binaries must be rebuilt per protocol line_withInnerWebClient+ depth-tracked re-entrancy fix_serializeWasmCall/mt+/mt/lazysubpaths, nightly pin)mt-threads×nodejsguarded bycompile_error!; MT diagnostics additionally browser-gatedinspectabledropped fromJsAccountUpdate/JsStorageMapEntry/JsStorageSlot/JsVaultAssetuseConsumeownership,TransactionId.toHex()toHex; next-only test mocks updated to match real WASM behaviorBlockHeader.nativeAssetId()→ exposed asfeeFaucetId()FeeParameters::native_asset_id→fee_faucet_id; the wallet's fee/native-asset discovery depends on this getter, which was absent from next entirelyMidenClient.ready()/waitForIdle()/lastAuthError()SignCallbackError→last_sign_errorcapture pipeline (wallet'sensureSdkWasmReady+ auth-error classification depend on the first and last)changesgating, dist-tree WASM size walkauto-release-main.ymldeleted (main-branch-only workflow)Conflict-resolution policy
next's side for everything 0.15-shaped (workspace version 0.15.0, git-pinned miden-client +
MIDEN_CLIENT_REF, napi/node build, js_export structure); main's side for the MT build system and CI hardening; unions for CHANGELOG / CLAUDE.md / knip and the exports maps (node condition +dist/stlayout +/mtsubpaths). Locks regenerated, never hand-merged.Verification
make format-check/toml-check/typos-check/clippy-wasm✅cargo clippy/build -p miden-mobile-prover(native, release) ✅pnpm build(ST + MT + types + node glue) ✅;check:wasm-types,check:standalone-types,check:method-classification✅pnpm run test— 1130/1130 ✅; reacttypecheck✅check:publint+check:attwon the merged exports maps ✅;check:knip✅useWorker/newCallbackProver/ready/feeFaucetId, all restored here) — wallet PR will carry theWeb SDK PR:markerPlease merge this with a true merge commit, not squash. The previous unification (#39) was squashed, which erased the merge parentage — that is exactly why all 43 of these commits re-conflicted this time. A merge commit makes the next main→next sync trivial.
Release
After this lands, a separate
patch releasePR should bump0.15.0-alpha.4→0.15.0-alpha.5so the wallet can consume the restored surface from npm. Release notes must flag: nightly toolchain now required for the MT build; new/mtsubpaths; react-sdk is now esm-only (cjs consumers need dynamic import); mobile-prover wire-format break for native plugins.