Skip to content

Latest commit

 

History

History
276 lines (185 loc) · 75.2 KB

File metadata and controls

276 lines (185 loc) · 75.2 KB

Changelog

1.15.3 (2026-06-27)

Features

  • [FEATURE][extension] New-wallet onboarding hands off to the Chrome side panel. When creating a wallet, the final onboarding screen creates it while the tab spins ("Creating your wallet…"); once it's ready, the "Open wallet" button opens the side panel onto the finished wallet and closes the onboarding tab. Creating the wallet first means the panel opens from a live user gesture (Chrome requires one for sidePanel.open()) onto an already-functional wallet — no loading state in the panel. The side panel also becomes the primary surface (clicking the toolbar icon opens it instead of the popup — toggle back via the header's maximise-view control). Import flows and non-Chrome browsers keep the classic in-tab flow.

Fixes

  • [FIX][all] Activating a device key on a migrated Guardian account no longer permanently breaks its sync. After a structural rotation (update_signers for replace-hot-key, or the follow-up update_procedure_threshold), the wallet now pushes the new on-chain state back to the guardian. The OpenZeppelin multisig client only re-registers post-execution state on the guardian for switch_guardian; for the other structural ops it submitted the transaction on-chain but left the guardian serving the pre-rotation blob, so every subsequent ~3s guardian sync saw guardian-commitment ≠ on-chain and threw on ensureSafeToOverwriteLocalState — permanently, until a full reinstall. The re-registration runs before the hot-key pointer swap arms the sync, and the guardian sync now self-heals a lagging guardian by re-registering the current state once per run (which also recovers already-broken accounts without a reinstall). The legacy-Guardian migration additionally verifies the re-derived cold key matches the on-chain signer before flagging an account for activation. (#227)
  • [FIX][all] Guardian sync no longer spams "missing hotPublicKey" errors for un-activated accounts. syncGuardianAccounts now only syncs Guardian accounts that actually carry a hot key, instead of every account not flagged requiresHotKeyRotation. A legacy single-signer Guardian record that hasn't been migrated yet — e.g. in the brief window after a wallet upgrade (new code, old storage) and before the forced re-unlock runs the migration — has no hot key, so getOrCreateMultisigService threw on it every ~3s AutoSync tick. There's genuinely no hot-bound service to build for such accounts, so they're skipped; recovery still happens via the migration → Activate Device Key banner path on the next unlock. (#227)
  • [FIX][all] Guardian operator endpoint is now tracked per account, and structural Guardian ops recover from a submit-then-local-apply failure. Each Guardian account persists its own guardianEndpoint (set at create/recovery, updated on switch-guardian) instead of a single global setting, so two Guardian accounts on different operators no longer collide — older records without the field fall back to the legacy global value. Separately, when a replace-hot-key or switch-guardian transaction lands on chain but the local apply step throws, the wallet now runs the same finalization the happy path would (swapping the hot-key pointer, or re-registering on the new guardian and persisting its endpoint) instead of cancelling — which previously stranded the account signing with a rotated-out key or talking to the old guardian. (#227)
  • [FIX][all] Guardian co-sign transactions are now serialized per account. The Guardian co-signs one delta per account at a time, so concurrent same-account transactions (e.g. auto-consume racing a user claim, or rapid successive claims) made the Guardian's expected commitment diverge from on-chain — stalling its canonicalization for minutes and returning 409 ConflictPendingDelta while a prior delta was still finalizing, which surfaced as a Guardian claim/send that never completes. Guardian transactions now take a per-account lock so at most one is ever in flight, and proposal creation waits out a transient 409 (a prior delta mid-canonicalization) instead of failing the transaction. (#297)
  • [FIX][mobile] iOS hot-key signing now requires Face ID / Touch ID on device, matching Android. The Secure Enclave hot key was created with .privateKeyUsage only — a usage permission that, contrary to the old code comment, does not prompt for authentication — so user-initiated Guardian claims and sends signed silently on iOS, while Android (StrongBox + setUserAuthenticationRequired) already prompted. New hot keys now also set .userPresence, so every user-initiated hot signature and hot-key reveal requires user presence (Face ID / Touch ID with passcode fallback). .userPresence is used rather than .biometryCurrentSet so the key survives biometric re-enrollment instead of bricking until re-activation. Background auto-consume is unaffected — it is cold-signed in WASM and never touches the hot key. Scope: the gate applies to device builds only (the simulator / iOS E2E path keeps .privateKeyUsage-only silent signing), and existing hot keys keep their prior behavior until re-activated/rotated. (#299)
  • [FIX][mobile] iOS app builds again under Xcode 26. Two foundKey as? SecKey downcasts in the hot-key plugin (signWithHotKey / revealHotKey) are no-ops for CoreFoundation types — they always succeed — which Xcode 26 now rejects as a hard error, breaking the iOS build. Replaced with a CFGetTypeID(foundKey) == SecKeyGetTypeID() guard plus a force-cast: both the correct defensive downcast and Xcode-26-clean. (#299)
  • [FIX][all] Guardian accounts can now connect to dApps (faucet, etc.) instead of failing with "Connection Failed" / NOT_GRANTED. A Guardian account's auth component is built by @openzeppelin/miden-multisig-client and its procedures live in the openzeppelin::auth::* MASM namespace, so they don't MAST-match any bundled miden-standards template. The SDK's AccountInterface therefore classifies the component as Custom and Account.getPublicKeyCommitments() returns []; the wallet's connect flow read that as "no public key" and rejected the connection (surfaced to the dApp as NOT_GRANTED). Public-key resolution now falls back, for accounts the SDK can't classify, to reading the hot signer's commitment directly from the account's openzeppelin::multisig::signer_public_keys storage map — the key the wallet actually signs with — so Guardian accounts resolve a usable session key. Plain single-key accounts are unaffected (their AuthSingleSig component is recognized as before). The same resolution covers the reveal-private-key and advanced-settings public-key views, which broke identically for Guardian accounts. (#300)
  • [CHANGE][ci] iOS E2E no longer hangs the full timeout when the simulator's CDP bridge wedges. CdpBridge.eval/evaluate now race the WebKit executeAtom call against a 30s hard timeout (matching evalAsync), so a wedged RWI socket or a momentarily-blocked WebView main thread surfaces as a fast throw instead of an indefinite await. Previously pollForCondition could only check its deadline between iterations, so a single hung eval stalled the whole test until Playwright's 15-minute kill (and the rest of the serial suite then skipped); now the poll enforces its own budget and --retries restarts on a fresh app + CDP. (#302)
  • [CHANGE][ci] Blockchain E2E retries the miden-client harness CLI on transient remote-prover connection failures. The CLI deploy/mint/sync retry loop classified only node-RPC and nonce-lag errors as transient; an intermittent TLS/gRPC handshake failure to the delegated prover endpoint on the macOS runners (failed to connect to the remote prover / transport error / no native certs found) was treated as fatal, so a mint failed outright even though a sibling mint in the same test connected fine. These connection-level prover errors are now recognized as transient and retried with backoff, and the three duplicated classifiers were unified into one isTransientCliError helper. (#302)
  • [CHANGE][ci] iOS E2E mobile jobs run on dedicated macos-26-xlarge runners, and are more resilient to degraded shared runners. The shared standard macos-26 runner pool was intermittently degraded for hours at a time by noisy-neighbour IO contention — every simctl op crawled (97 CI samples: per-wallet _simPair setup p50 65s, p90 267s, max 401s vs. <5s healthy — so two sequential wallets took up to ~13 min and sometimes never finished even in 15 min), making the whole mobile suite un-runnable. All four mobile E2E jobs now use the dedicated Apple-Silicon -xlarge larger runner (2× vCPU/RAM, no noisy neighbours), which restores a healthy ~2-3 min setup and a full green suite. As belt-and-suspenders for any residual slowness, the _simPair fixture setup is capped (at 13 min, past the slowest observed completing setup) so a genuinely-hung CoreSimulator fails fast with a named error and a sim-subsystem restart instead of silently eating the whole per-test timeout, while a degraded-but-completing setup is allowed to finish rather than being killed mid-flight (no assertion is relaxed — purely tolerance for degraded IO); the subsystem recovery simctl shutdown alls to clear half-booted device state (the SimError 405 signature); the per-test timeout is 25 min (from 15); and the non-guardian mobile suite runs with --retries=2 (matching the guardian suite). (#302)
  • [CHANGE][ci] Guardian iOS E2E reads the on-chain auth structure with a pure storage parse instead of loading the multisig client. The verify_guardian_auth_structure assertion's __TEST_GUARDIAN_AUTH__ hook used to build a MultisigService (getOrCreateMultisigServiceMultisigClient.load) and read it. Against the post-consume state — where the guardian's stored blob lags the on-chain account — that load entered a re-sign/realign loop (~48 signWithHotKey calls for this read vs. 26 for a full consume) that hung the single-threaded mobile WASM past the eval budget; the assertion never got far enough to run on iOS. The structure (signer set + procedure thresholds) is immutable and lives in the account's storage maps, so the hook now reads it directly with AccountInspector.fromAccount — a pure parse with no signing, no guardian HTTP, and no client load (just one getAccount, the same read the balance poll already does). Because even that lone getAccount was still starved by other main-thread WASM activity on iOS (the auth eval was observed taking 60s with all the wallet's own pollers paused), the structure is now captured in the wallet's own balance poll (fetchBalances, which reliably completes) and stashed on a global; the test reads it as a plain value with no WASM call at all. Finally, the iOS harness reads that stash over the SYNCHRONOUS execute_script atom (polled), not the async execute_async_script one: appium-remote-debugger's async atom delivers its completion callback in the arguments[arguments.length-1] slot as the boolean true on this iOS RWI bridge, so cb(result) threw TypeError: cb is not a function, the promise rejected unhandled, and every evalAsync hung to its timeout no matter how fast the script ran — which is why the auth read still timed out at 60s even with the stash already populated. Test-only, gated on MIDEN_E2E_TEST and tree-shaken from production. (#302)

1.15.2 (2026-06-22)

Features

  • [FEATURE][all] Custom-transaction support for Guardian accounts. Guardian accounts can now co-sign arbitrary transaction requests, not just sends and note consumption: the multisig client builds a custom proposal from the request bytes, co-signs it through the Guardian, and reconstructs the executable TransactionRequest from the Guardian's advice. (#153)

Changes

  • [CHANGE][all] Guardian now runs on the Miden 0.15 protocol line. The OpenZeppelin Guardian client packages (@openzeppelin/miden-multisig-client, @openzeppelin/guardian-client) are pinned to 0.15.0-rc.0 (built against @miden-sdk/miden-sdk ^0.15.0), replacing the published 0.14.9 — which targets SDK 0.14.5 and trapped with RuntimeError: memory access out of bounds inside MultisigClient.create() when run on the wallet's 0.15 SDK (WASM ABI skew between the 0.14 client and the 0.15 SDK). Guardian account creation, note consumption, and sends now work end-to-end on 0.15. Guardian currently requires devnet: the hosted devnet Guardian runs the 0.15 server, while the testnet/production Guardian is still on the 0.14 server and is incompatible with the 0.15 client — until OpenZeppelin publishes a 0.15 Guardian server release. (#153)
  • [CHANGE][all] Bumped @miden-sdk/miden-sdk and @miden-sdk/react from 0.15.1 to 0.15.2 (and the matching **/@miden-sdk/miden-sdk resolution pin). @miden-sdk/vite-plugin stays at 0.14.11. (#153)

Fixes

  • [FIX][all] Corrected the devnet Guardian endpoint host (stg-guardian.openzeppelin.comguardian-stg.openzeppelin.com); the transposed hostname had no DNS record, so the default devnet Guardian URL never resolved. (#153)
  • [FIX][extension] Chrome package no longer bundles a second (single-threaded) Miden SDK WASM. @openzeppelin/miden-multisig-client imports the eager @miden-sdk/miden-sdk entry; the service-worker config already redirected that to the multi-threaded build, but the extension/front config did not — so the front bundle pulled in an extra ~15 MB single-threaded WASM. The front config now applies the same eager→mt/lazy alias, restoring the Chrome package to ~18 MB (was ~29 MB).

1.15.1 (2026-06-19)

Changes

  • [CHANGE][all] Bumped @miden-sdk/miden-sdk and @miden-sdk/react to the stable 0.15.1 release, replacing the 0.15.0-alpha.7 prerelease (npm next dist-tag) that shipped in 1.15.0 with the GA build on the latest dist-tag. @miden-sdk/vite-plugin stays at 0.14.11 (no 0.15.x is published; it's SDK-version-agnostic), and the **/@miden-sdk/miden-sdk resolution pin tracks the same 0.15.1.
  • [CHANGE][ci] Mobile E2E iOS build now targets a generic simulator destination (generic/platform=iOS Simulator) instead of a named device (name=iPhone 17), matching the approach build-mobile.yml's release build already uses. A build needs no concrete booted device, and the generic destination resolves even when Xcode's IDE-layer CoreSimulator goes blind to concrete sims on macos-26 runners — the flake that was failing Mobile E2E at the build step (xcodebuild: Unable to find a device). The now-redundant "wait for xcodebuild to see the simulators" gate is removed (it could hard-fail the job for a condition that no longer blocks the build); pinned sims are still pre-booted for the test-run phase, which reaches them via simctl.

1.15.0 (2026-06-12)

Features

  • [FEATURE][all] 3-key Guardian: cold co-signs switch_guardian (on-chain threshold-2 satisfied via procedureThresholds) and a new Settings → Rotate Device Key flow lets users replace the hot signer in-place via a single cold-signed update_signers proposal, persisting the new ciphertext before submission and finalizing WalletAccount.hotPublicKey (plus releasing the old SE/StrongBox wrapper) only after on-chain inclusion. Settings → Switch Guardian now requires an explicit confirmation step before initiating, mirroring the Rotate Device Key flow so the user acknowledges that the switch is cold-signed + guardian-co-signed. Guardian import-by-seed does lookup + adopt only (MultisigClient.recoverByKey per HD index until first miss); each adopted account is flagged requiresHotKeyRotation and the home view surfaces an Activate Device Key banner with a one-click CTA that fires the same cold-signed update_signers rotation — the banner self-hides once Vault.swapHotKey lands. The swapHotKey message now carries only newHotPubKey; the vault resolves the previous hot from the persisted WalletAccount, so the initial post-recovery activation and subsequent rotations share one code path.
  • [FEATURE][all] 3-key Guardian accounts. Guardian creation now provisions a hot ECDSA key (random, lives outside the WASM keystore behind a new secure-hot-key facade — extension/desktop use a JS fallback; iOS wraps the secret under a per-account Secure Enclave P-256 key via HotKeyPlugin.swift; Android wraps it under a per-account StrongBox-backed RSA-OAEP key via HotKeyPlugin.kt with biometric-gated unwrap, identical wire format on both platforms) plus an HD-derived cold ECDSA key (kept in the SDK keystore for cold-routed flows) alongside the existing guardian co-signer. Threshold stays at 1 (hot OR cold + guardian); cold-only routing for update_signers / update_guardian / update_procedure_threshold is enforced client-side per the Phase 0 SDK reading. Storage adds accColdSecretKeyStrgKey and WalletAccount.{hotPublicKey,coldPublicKey} so role-aware signWord (Phase 3) can dispatch hot vs cold by storage entity. Hard cutover from the 1-of-1 Falcon scheme — pre-cutover Guardian accounts are unreachable from this build.
  • [FEATURE][all] Guardian integration. Adds Guardian-backed accounts (1-of-1 multisig with on-chain Guardian signature verification), onboarding create/import flows with a dedicated recovery-method screen that accepts a custom guardian URL, Settings → Guardian Settings with an on-chain switch-guardian proposal that re-registers post-switch state with the new endpoint, service-worker routing for Guardian transaction signing via MultisigService, frontend Guardian sync outside the WASM lock, and per-stage progress labels (creating-proposal, signing-proposal, submitting, registering-guardian) in the transaction modal.
  • [FEATURE][all] Default auth scheme for new accounts switched from Falcon to ECDSA. Existing accounts are unaffected — Miden seals the auth component at on-chain creation and can never rotate it, so any account previously created stays Falcon and continues signing with its existing keystore secret. Restore paths handle both schemes: mnemonic-only restore (Vault.spawn) probes the chain under each scheme to find the user's actual hdIndex=0 account; encrypted-file restore reads the new optional authScheme field on WalletAccount (legacy entries with no field default to Falcon, matching the historical default 1:1). Private-key import detects the scheme from the deserialized AuthSecretKey via the SDK's per-scheme accessor. New accounts created post-upgrade get ECDSA stamped into their WalletAccount record. Encrypted-file format change is purely additive — old files round-trip through restore as Falcon. (#229)

Changes

  • [CHANGE][all] Migrated to the Miden 0.15 protocol line@miden-sdk/miden-sdk and @miden-sdk/react bumped to the 0.15.0-alpha series (npm next dist-tag; @miden-sdk/vite-plugin stays at 0.14.11, which is SDK-version-agnostic). User-visible consequences of the protocol bump:
    • Local stores do not carry over. 0.14 account IDs, note IDs, and nullifiers do not round-trip under 0.15 (account-ID version renumbered, note identity split into details-commitment + metadata-bearing ID, hashing changed). The SDK's IndexedDB store detects the version bump and re-creates itself; accounts re-register from the wallet seed and balances resync on unlock. Saved 0.14 wallet store files (Settings → export) cannot be imported into a 0.15 build.
    • Faucets minted by pre-0.15 SDKs can no longer be introspected for token metadata (BasicFungibleFaucetComponent.fromAccount reads the new metadata slot); their assets now display as "Unknown" instead of being blacklisted for the session.
    • The dApp ImportPrivateNoteResponse.noteId for details-only note imports (the common noteBytes path) now carries the note's details-commitment hex rather than a note-id hex, following notes.import's new return contract.
    • Network/fee-asset discovery follows the protocol rename (BlockHeader.feeFaucetId(), formerly nativeAssetId()); the discovery cache is keyed v2 so 0.14-cached IDs are not reused.
    • Account storage modes are public/private only (the chain's separate network-account flag is gone), and partial (metadata-less) input notes — which have no note ID until sync completes them — are filtered out of consumable/claimable listings.
  • [CHANGE][ci] Blockchain E2E installs miden-client-cli from a pinned git rev (midenClientCliGit in package.json) while the 0.15 CLI is unreleased on crates.io; testnet E2E is expected red until testnet upgrades to node 0.15 (devnet already runs it) — covered by the at-least-one-network gate.

Fixes

  • [FIX][all] Guardian sync no longer leaks per-init web-client workers. MultisigService.init now reuses the shared getMidenClient() singleton instead of a throwaway MidenClientInterface.create({}) client, and getOrCreateMultisigService coalesces concurrent init calls with an in-flight promise map so 3s sync ticks cannot stampede slow or failed initialization.
  • [FIX][mobile] Wallet creation no longer panics the WASM client on iOS/Android (RefCell already borrowed → poisoned instance → claims and balances silently dead). On mobile the front-end and wallet back-end share one direct-path (useWorker: false) SDK client, and the front-end's balance polling overlapped the wallet-creation syncs; @miden-sdk/miden-sdk@0.15.0-alpha.7 restores the SDK-side call serialization that makes those overlaps queue instead of panic (web-sdk#184).
  • [FIX][extension] The offscreen prover document now constructs the SDK's raw wasm-bindgen WebClient (prover-only — createClient() is never called) instead of the worker-shim wrapper. The wrapper forwarded every prove to its own method worker — a WASM instance whose rayon pool the offscreen document never initialized — and its implicit worker INIT performed a network round-trip against the SDK's default RPC endpoint, making local proving silently dependent on that endpoint being reachable and version-compatible. Proves now run on the offscreen document's own thread pool with no implicit network access (so local proving works offline), completing in ~5–6 s on a 10-core machine. Requires @miden-sdk/miden-sdk0.15.0-alpha.6 (the explicit-prover fix in web-sdk#182). Root-cause analysis in web-sdk#180.

1.14.7 (2026-06-09)

Fixes

  • [FIX][extension] Local proving (chrome.offscreen MT-WASM path) no longer hangs the wallet. Every consume and send with delegate_proof_setting_key=false would deadlock inside the SDK's _withInnerWebClient callback at inner.getInputNote(...) — the callback's outer _serializeWasmCall slot awaited the inner _serializeWasmCall slot that was queued behind it (classic re-entrant-lock deadlock; see web-sdk#152 for the SDK-side root cause + fix). Downstream, the wallet's SyncManager saw the wasm-client mutex held for the full hang window and tripped its 5 s timeout three times, opening the circuit breaker and showing the user a misleading "cannot reach the miden node" banner. With the SDK fix in place, consumeNoteId / sendTransaction / newTransaction complete normally on the offscreen-doc prove path. Requires @miden-sdk/miden-sdk@0.14.10.
  • [FIX][extension] SyncManager no longer surfaces a "cannot reach the miden node" banner during legitimate long local proves. Previously the 5 s SYNC_TIMEOUT_MS bounded the combined lock acquisition + RPC call, so any wasm-client lock held >5 s by an in-flight prove looked indistinguishable from a real RPC stall — three of those in a row tripped the circuit breaker and markConnectivityIssue('node'). The timeout now wraps the RPC alone; queued syncs wait patiently behind the prove (which yields the lock via yieldWasmClientLock around the offscreen step, so the wait is short), then bound only the actual network call. Steady state correctness is unchanged: a sync after the yield clears any active reachability category as before.
  • [FIX][all] Stale transaction-completion modal no longer blocks subsequent sends. After PR #230 the TransactionProgressModal auto-dismiss was correctly gated on terminal-state signals so the "Tx Completed → View on Midenscan" screen wouldn't be ripped away when the success-path navigate('/') fires after a long local prove — but with no other dismissal path the modal stayed up as a full-viewport zIndex: 9999 overlay until the user explicitly tapped Done. Stress tests (and any user starting a second send before tapping Done) navigated to /send, found the SelectToken tile blocked behind the modal, and saw locator.click time out against getByTestId('send-flow').locator('div.cursor-pointer') in 80–99.5% of attempts. SendManager now closes any stale completion modal on entry — an explicit "starting a new tx" signal equivalent to tapping Done. Initially shipped in #245 gated on lastCompletedTxHash !== null, which only covered send completions; stress runs against #245 showed claim/dApp completions still left the modal sticky (the previous turn's recipient could not start its send because its post-claim completion modal blocked the SelectToken tile). Widened in this release to drop the lastCompletedTxHash gate — every open completion modal closes on send-flow entry, covering the Receive / ConfirmPage / SendManager modal-open paths uniformly. In-flight modals can't reach this code path because PR #217's pathname-watching effect in the modal already auto-dismisses non-terminal opens on navigation away.
  • [FIX][all] The "Cannot reach the Miden node" banner no longer flaps on a slow-but-healthy node. Two issues compounded: (1) SYNC_TIMEOUT_MS was 5s while a testnet sync can legitimately take 5-25s, so healthy syncs routinely tripped the watchdog — and the timeout never actually freed the WASM client mutex early (withTimeout only rejects the outer promise; the underlying syncState keeps running and holding the lock until it settles), so the aggressive ceiling bought nothing and only manufactured false failures; (2) markConnectivityIssue ran on the first failure, before the circuit-breaker check, so a single slow sync surfaced "node unreachable" even while block height was still advancing. SYNC_TIMEOUT_MS is raised to 30s (a true wedged-sync watchdog), and the connectivity banner is now gated on the same MAX_CONSECUTIVE_SYNC_FAILURES (3) streak that opens the circuit breaker, so it only appears when the node is persistently unreachable and clears on the next successful sync. Reported in #252.

Changes

  • [CHANGE][all] Bumped the Miden SDK to 0.14.11@miden-sdk/miden-sdk, @miden-sdk/react, and @miden-sdk/vite-plugin are kept in lockstep (with the **/@miden-sdk/miden-sdk resolution pin) to avoid mismatched-version WASM/type issues.

1.14.6 (2026-06-03)

Fixes

  • [FIX][extension] A failed public-key fetch during dApp connect no longer leaves the wallet permanently stuck on "Connecting…". In the extension connect path (generatePromisifyRequestPermission in src/lib/miden/back/dapp.ts), if getAccountPublicKeyB64 threw (e.g. the account was momentarily unavailable during instability), the error was swallowed and a dApp session was persisted with publicKey: null. The direct-return path then handed that null public key back verbatim on every subsequent connect — with no confirmation popup — so the dApp's connect() never resolved and the only escape was clearing the session or reinstalling. The extension branch now fails closed (rejects with NotGranted, no session saved), mirroring the non-extension branch which already threw on the same failure, so a transient error surfaces as a clean, retryable rejection instead of a permanent wedge. Regression test added. Reported in #219.

  • [UX][devnet] Devnet build icon now carries a "developer" badge. The devnet extension icon (public/misc/logo-devnet*.png) gains a small wrench badge — the Advanced Settings / Developer glyph in a white circle with a thin #7286A0 ring — overlapping the m.'s bottom-right, so a devnet build is distinguishable at a glance beyond the existing blue-vs-orange color. Regenerated at every size (16/32/40/48/128 + the 234px master).

  • [FIX][devnet] Devnet tab favicon now matches the devnet icon. The wallet's tabbed pages (fullpage.html) hard-coded the orange logo-white-bg favicon, so a devnet wallet opened in a browser tab showed the orange production logo. The extension build's transformIndexHtml now swaps the favicon to logo-devnet for MIDEN_NETWORK=devnet, mirroring the existing devnet manifest-icon swap.

  • [FIX][all] Legal links no longer 404. The Privacy Policy and Terms links pointed at the dead miden.fi domain in two places — the onboarding password screen (CreatePassword) and Settings → About — and the Settings → About entries additionally rendered <a href="#"> because they used slug: '#'. New PRIVACY_POLICY_URL / TERMS_OF_USE_URL constants in src/app/constants.ts point at the live https://0xmiden.github.io/wallet/privacy/. There is no dedicated Terms page yet, so TERMS_OF_USE_URL temporarily resolves to the Privacy URL until one ships — flip the constant when it does. Reported in #202 and #219.

  • [FIX][all] Advanced Settings now shows the account public key. The "Account Public Key" row rendered only a label with no value, and the copy button copied nothing while the key was still resolving. It now shows the truncated key (0x + first 6 + + last 4), reserves the row height with a non-breaking space so it doesn't jump on first paint, and disables the copy button until the key is available. Reported in #219.

  • [FIX][all] The toolbar close (✕) button is now visible in dark mode. The PageLayout close button passed a literal fill="black" to its Icon, so the ✕ was invisible against the dark app background on every page that uses the toolbar (HistoryDetails was the reported case, but the bug was global). It now passes fill="currentColor", inheriting the button's text-black token (black in light, white in dark). Reported in #219; follow-up to #265/#266.

  • [FIX][all] Activity loaders are now visible in dark mode. The pending-transaction spinner in the activity list (rotate.svg, rendered by TransactionIcon) hardcoded fill="black", so it was invisible on the dark app background. It now uses fill="currentColor" driven by a text-black class (var(--color-text-primary)), rendering black in light and white in dark — matching the app's other currentColor icons. The shared Loader (components/Loader.tsx) also defaulted color to 'black'; it now defaults to 'currentColor' so a bare <Loader/> inherits the themed text color instead of painting literal black. The header sync-spinner track moved from a hardcoded #E5E7EB to var(--color-border-secondary) (the orange progress arc is unchanged). Follow-up to #265; reported in 0xMiden/wallet-adapter#90 (item 3). (#266)

  • [FIX][all] Dark mode now themes the whole UI. The wallet was built light-first: ~160 sites across screens hardcoded Tailwind color utilities (the grey-* ramp, gray-200..900, arbitrary [#hex], pure-white) that resolve to fixed values regardless of theme. Theming is driven by darkMode: 'class' + CSS custom properties in src/main.css, but these hardcoded utilities never flipped — so in dark mode light-gray captions vanished, inputs/cards/modals stayed white, hairline borders disappeared, and Navigator's page-transition backgroundColor painted every routed page #ffffff. All such sites now use the var-backed semantic tokens that flip with the .dark class (text-text-muted, text-heading-gray, border-border-card/-light, bg-surface-solid/-chip-bg/-input-bg, bg-gray-25/50/100); Navigator reads var(--color-app-bg); and pastel status alerts gained dark: fill/text variants (using valid -500 text shades, since this palette has no red/green -300). The dApp browser surfaces (confirmation modal, load-error overlay, capsule bar, launcher) were co-themed so a themed background never leaves dark-on-dark text. Brand colors, white-on-saturated buttons, status accents, QR codes, dark scrims, and the inverted-selected chip are intentionally unchanged. 96 files, color utilities only — no logic or copy changes. Verified at the CSS/build level (tsc, eslint, build:mobile clean; generated utilities map to var(--color-*) and flip under .dark); runtime pixel verification still recommended. (#265)

  • [FIX][all] Consuming a note via the dApp API can no longer brick the wallet. A dApp's requestConsume may carry serialized note bytes via ConsumeTransaction's noteBytes parameter, which the wallet enqueues (queueNoteImport) and later imports via NoteFile.deserialize in importAllNotes (src/lib/miden/activity/notes.ts). That parameter has an undocumented, unvalidated requirement: it must be a serialized NoteFile. The natural thing for a dApp to pass — note.serialize() — produces a serialized Note instead (a different container, with no "note" magic header), so deserialization throws notefile deserialization failed: invalid utf-8 sequence.... The note itself is valid; only the serialization container is mismatched. (The companion fix below makes the wallet accept a Note here, so this specific mismatch no longer fails at all.) Previously importAllNotes aborted the whole batch on any failure and only cleared the persistent import queue (miden-notes-pending-import) after full success, so the bad note was retried on every transaction-loop iteration forever. Because importAllNotes() is the first call in generateTransactionsLoop, this permanently jammed ALL transaction generation (sends and consumes alike) — the queue lives in persistent storage, so the jam survived restarts and only a reinstall recovered. importAllNotes now imports each note independently (one failure no longer aborts the batch) and applies a bounded retry: a failing note is kept and retried, carrying an attempt count, and dropped only after MAX_IMPORT_ATTEMPTS (3) failures. This keeps the queue draining — a deterministically bad note can never loop forever and re-brick the wallet — while still giving genuinely transient failures (e.g. a NoteFile::NoteId import, which fetches the note over RPC and can hit a network blip) a chance to succeed, so a recoverable note (including a private note whose bytes are its only copy) isn't lost to a single blip. Processed-note removal happens inside the WASM lock and before syncState, so a sync failure can't leave processed notes queued for an unbounded retry. Queue entries gain an { bytes, attempts } shape; legacy bare-string entries are normalized on read, so no migration is needed. Regression tests cover a failing note kept for retry with an incremented count, a poison note dropped after 3 attempts without importAllNotes ever throwing, the queue draining even when syncState throws, and concurrently-enqueued notes preserved across a pass. Reported in 0xMiden/wallet-adapter#87.

  • [FIX][all] Importing note bytes now accepts a serialized Note, not just a NoteFile. MidenClientInterface.importNoteBytes (src/lib/miden/sdk/miden-client-interface.ts) previously called NoteFile.deserialize directly, so any caller passing a serialized Note — the natural output of note.serialize(), and what a dApp typically has on hand for ConsumeTransaction's undocumented noteBytes parameter — failed with notefile deserialization failed: invalid utf-8 sequence.... It now accepts either form: a NoteFile is used directly, and a bare Note is wrapped into a NoteFile (the NoteDetails variant, mirroring NoteFile.fromInputNote when no inclusion proof is available) before import. Bytes that are neither raise a clear, actionable error (bytes are neither a serialized NoteFile nor a serialized Note...) instead of the opaque deserialization failure. This is the trigger behind 0xMiden/wallet-adapter#87: combined with the brick fix above, a dApp consuming a note via requestConsume with note.serialize() bytes now works end to end. Covers the consume path, custom-transaction importNotes, and the extension's import-note request, since all route through importNoteBytes. Reported in 0xMiden/wallet-adapter#87.

  • [FIX][dapp] Generalized requestTransaction now handles send / consume, not just custom. A dApp consuming a note through the typed Transaction(TransactionType.Consume, new ConsumeTransaction(...)) API (rather than the dedicated requestConsume) hit INVALID_PARAMS: Invalid CustomTransaction payload: the wallet's requestTransaction handler (generatePromisifyTransaction in src/lib/miden/back/dapp.ts) read req.transaction.payload and validated it as a MidenCustomTransaction unconditionally, ignoring the tagged MidenTransaction.type. It now dispatches by typesend delegates to the send flow (generatePromisifySendTransaction) and consume to the consume flow (generatePromisifyConsumeTransaction), reusing their existing preview / confirmation / execution paths, while custom (and bare/legacy payloads) keep flowing through the custom path. The type discriminant is compared by its wire string value rather than importing the adapter-base TransactionType enum (that package is ESM-only and consumed type-only here). Fixes 0xMiden/wallet-adapter#88.

  • [FIX][mobile] dApp wallet-connect no longer fails. Two cascading bugs broke every window.midenWallet.connect() call on iOS + Android — the extension was unaffected because it goes through a different confirmation path. First crash: getNetworkRPC() in src/lib/miden/back/dapp.ts used a non-null assertion (NETWORKS.find(n => n.id === net)!.rpcBaseURL) — when a dApp called connect() without passing a network argument (which the testnet faucet, and any dApp using the standard @demox-labs/miden-wallet-adapter-base API, does — they just want to use whichever network the wallet is on), the find() returned undefined and the wallet threw Cannot read properties of undefined (reading 'rpcBaseURL') immediately. The error was wrapped into a MIDEN_PAGE_ERROR_RESPONSE and shipped back to the dApp before the permission modal ever rendered. getNetworkRPC now falls back to getCurrentMidenNetwork() when net is undefined and throws a clean NetworkNotGranted for genuinely unknown ids. Second crash, hidden behind the first: DappBrowserProvider was passing shortAccountId (the truncated mtst1apsnk6...qq9wr6w display string with literal ...) into DappConfirmationModal. The modal echoed that truncated string back as accountPublicKey on Approve. The backend then called midenClient.getAccount(accountId) with the truncated form, the WASM bech32 decoder threw invalid character (code=.), and requestPermission mapped it to NOT_GRANTED — so the dApp saw the permission denied even though the user had tapped Approve. The modal now receives the full bech32, truncates locally only for display via truncateAddress(), and sends the full id back through onResolve. End-to-end CDP repro on Android emulator confirms connect() now resolves with {accountId, publicKey, privateDataPermission, allowedPrivateData} in ~430 ms. Regression tests cover both: getNetworkRPC(undefined) returns the current network's RPC, getNetworkRPC('unknown') throws NetworkNotGranted, and the modal echoes the full bech32 (no ...) on Approve.

  • [FIX][all] Stale transaction-completion modal no longer blocks subsequent sends. After PR #230 the TransactionProgressModal auto-dismiss was correctly gated on terminal-state signals so the "Tx Completed → View on Midenscan" screen wouldn't be ripped away when the success-path navigate('/') fires after a long local prove — but with no other dismissal path the modal stayed up as a full-viewport zIndex: 9999 overlay until the user explicitly tapped Done. Stress tests (and any user starting a second send before tapping Done) navigated to /send, found the SelectToken tile blocked behind the modal, and saw locator.click time out against getByTestId('send-flow').locator('div.cursor-pointer') in 80–99.5% of attempts. SendManager now closes any stale completion modal on entry — an explicit "starting a new tx" signal equivalent to tapping Done. Initially shipped in #245 gated on lastCompletedTxHash !== null, which only covered send completions; stress runs against #245 showed claim/dApp completions still left the modal sticky (the previous turn's recipient could not start its send because its post-claim completion modal blocked the SelectToken tile). Widened in this release to drop the lastCompletedTxHash gate — every open completion modal closes on send-flow entry, covering the Receive / ConfirmPage / SendManager modal-open paths uniformly. In-flight modals can't reach this code path because PR #217's pathname-watching effect in the modal already auto-dismisses non-terminal opens on navigation away.

1.14.4 (2026-05-28)

Fixes

  • [FIX][mobile] Mobile sync no longer hangs on WKWebView. The SDK's Web Worker shim runs every client.* call (including syncState()) inside a worker, but WKWebView Workers can't complete a gRPC-web fetch — so the very first auto-sync after wallet creation would hang forever, leaving isSyncing: true and lastSyncedAt frozen, with newly-minted public notes never surfacing in the Receive screen. Wallet now passes useWorker: false to MidenClient.create on mobile (the option lands in @miden-sdk/miden-sdk@0.14.9 from web-sdk#149), so the SDK runs in the main thread on mobile (mirroring the pre-0.14.4 behavior). Desktop / Chrome extension keep useWorker: true (the Worker shim is fine there). Web SDK PR: #149.

Features

  • [FEATURE][mobile] Native Rust transaction prover on iOS + Android. On opt-in (Settings → General → Local proving), the wallet hands the SDK a CallbackProver whose closure routes prove calls through a new Capacitor plugin (@miden/native-prover) → JNI / Swift FFI → LocalTransactionProver linked as a static / shared lib. Same wire bytes as RemoteTransactionProver, so the SDK dispatch path is unchanged downstream. Single-consume timings: iOS 17 sim 1.575 s native prove (2.07 s end-to-end), iOS 17 real device ~3 s, Android arm64 emulator 4.99 s (5.12 s with base64 + IPC), expected ~3 s on real Android hardware. Replaces the prior fallback to the SDK's in-worker WASM single-thread prover (60–90 s on sim) — roughly 50–80× faster. Backwards-compatible: DEFAULT_DELEGATE_PROOF stays true, the native path is opt-in, and the Chrome extension is unaffected (worker shim stays enabled, no CallbackProver is constructed). Requires web-sdk's new TransactionProver.newCallbackProver(jsFn) + ClientOptions.useWorker: false (web-sdk #149) to keep the closure alive past the WebClient dispatch boundary. Ships an Android Playwright E2E harness alongside (playwright/e2e/android/*) mirroring the existing iOS one. Web SDK PR: #149. See 0xMiden/protocol#2906 for the underlying miden-tx 0.14.6 regression (asset-callback kernel rewrite that broke every consume) — resolved by the upstream yanking 0.14.6, so 0.14.5 stays the resolver pick and no pin is needed on this branch. (#235)

  • [FEATURE][ci] Linked Web SDK PR pattern. Wallet PRs that depend on an unpublished @miden-sdk/miden-sdk or @miden-sdk/react change can now include Web SDK PR: #N in the description. CI's new .github/actions/inject-linked-web-sdk-pr action clones the linked web-sdk PR, builds the SDK packages from source, and rewrites package.json to consume them via file: deps — so the wallet PR's CI builds against the actual upstream change without requiring it to publish first. A separate check-linked-web-sdk-pr.yml workflow keeps a linked-web-sdk-pr-ready commit status pending until the linked PR is merged AND a web-sdk release tag covering its merge commit is published, with a 15-min cron re-evaluation so the gate auto-flips green without re-pushing. Local-dev parity via scripts/dev-with-web-sdk-pr.sh (with --clear); a new lefthook.yml pre-commit hook blocks committing the patched state. Mirrors web-sdk's existing Client PR: #N pattern. See "Linked Web SDK PR (cross-repo CI)" in CLAUDE.md for full details. (#231)

  • [FEATURE][extension] Multi-threaded local proving + speculative pre-prove. Switched the SDK import path from @miden-sdk/miden-sdk/lazy (single-threaded WASM, no COI requirement) to @miden-sdk/miden-sdk/mt/lazy (multi-threaded WASM with wasm-bindgen-rayon, requires cross-origin isolation). The wallet's MV3 manifest already declares COOP=same-origin + COEP=require-corp, so the requirement is satisfied automatically. Replaces the SW-bundled single-threaded prove path (~25-40s on a typical laptop) with a chrome.offscreen document hosting wasm-bindgen-rayon over navigator.hardwareConcurrency threads (~5-10s). The SW orchestrates execute → offscreen prove → submit → apply, releasing the WASM-client mutex during the prove via a new yieldWasmClientLock so background sync stays responsive across the multi-second prove window. A new SpeculationManager kicks off the prove the moment the SendDetails form is valid (debounced 500 ms) so by the time the user taps Confirm the proof is usually already cached — submit + apply runs in ~250 ms. If the user taps Confirm while the prove is still in flight, the wallet awaits the in-flight matching speculation rather than starting a duplicate. If the user changes form params mid-prove, the offscreen doc is terminated to abort the now-stale prove (cores freed for the new params); a non-speculative-prove counter blocks the abort whenever a real send / consume / new-transaction prove is in flight, so the user's actual transaction is never killed. The per-send "Delegate proving" toggle on the Send screen was dropped — local vs. delegated is now driven exclusively by Settings → General → Local proving. Off by default; built-time MIDEN_USE_OFFSCREEN_PROVING=true and MIDEN_USE_SPECULATIVE_PROVING=true enable it (defaulted on for desktop Chrome builds via build:chrome). Mobile keeps remote proving — there's no equivalent chrome.offscreen API on mobile platforms, and the mobile build pins both flags to false.

  • [FEATURE][all] Private key export & import. Settings → Reveal Private Key now shows the hex-encoded auth secret for the current account (guarded by the existing password / biometric unlock). Import Account → Private Key accepts a hex secret plus optional name and rebuilds the account deterministically via AccountBuilder + AccountComponent.createAuthComponentFromSecretKey; the secret is persisted through the existing keystore.insertinsertKeyCallback path, so the reveal/sign pipeline treats imported accounts identically to HD-derived ones. Imported accounts are tagged hdIndex: -1. (#195)

  • [FEATURE][all] Transaction-complete modal now surfaces a View on Midenscan action alongside Done. Desktop / extension opens the explorer in a new tab; mobile opens it as a native InAppBrowser overlay so dismissing the overlay returns the user to the completion screen with no state loss. URL resolved per-network via a new MIDEN_EXPLORER_ENDPOINTS map (testnet / devnet); localnet has no explorer → button hidden. The on-chain tx hash is plumbed through SendManager.onSubmitlastCompletedTxHash in the Zustand store, cleared at the start of each send so the button never points at a stale hash. (#203)

  • [UX][all] Transaction-complete modal no longer auto-closes 3 s after success — user now dismisses explicitly, giving time to read the confirmation and tap View on Midenscan. (#203)

Fixes

  • [FIX][all] Transaction-progress modal no longer auto-dismisses on the SendManager's success-path navigate('/') when the prove takes longer than the modal's 2-second post-open grace window. The auto-dismiss (PR #217) was sized for delegated proving (~1-2 s round trip); local proving runs in 5-10 s, so the success-path navigate fires WELL after the grace expires and used to close the modal before the user could see the "Tx Completed → View on Midenscan" screen — they'd land on Home with no visible result. The auto-dismiss is now gated on terminal-state signals (lastCompletedTxHash set, transactionComplete true, or hasErrors true); while the tx is genuinely in flight it still fires (preserving PR #217's click-through-blocker fix), but once we have a result the modal stays open until the user explicitly taps Done.

  • [FIX][all] Connectivity-issue surface revamped end-to-end. The single misnamed connectivity-issues flag (which only ever fired for prover failures, persisted indefinitely after a single transient 502, and was never wired on mobile) is replaced by a categorized state machine in lib/miden/activity/connectivity-state.ts tracking network (user offline), node (Miden RPC unreachable), prover (remote prover down), and resolving (recovery probe in flight) independently. Categories auto-clear on the next successful operation of the relevant kind: prover clears on every successful withProverFallback invocation; network/node/resolving clear on every successful sync from both the SW (sync-manager.doSync) and the in-process mobile/desktop sync loop (useSyncTrigger). The banner in Explore now picks the highest-priority active category (network > node > prover > resolving), shows category-specific copy and an actionable Retry CTA where it helps (no CTA for prover, since fallback to local proving is already silent), and is no longer gated behind !isMobile(). The dead sendConnectivityIssue runtime-message path was retired (the ExtensionMessageListener is now a no-op). New i18n keys: connectivityNetworkTitle/Body, connectivityNodeTitle/Body, connectivityProverTitle/Body, connectivityResolvingTitle/Body, connectivityRetry, connectivityRetrySync. New tests cover the state-machine transitions, the classify heuristic (network vs node vs semantic-error), the prover-success clear path, and the sync-error categorization. (#141)

  • [FIX][mobile] Unblocked app boot on iOS/WKWebView by switching lib/miden-chain/native-asset.ts from @miden-sdk/miden-sdk (eager WASM entry) to @miden-sdk/miden-sdk/lazy. This was the one straggler missed by the 1.14.2 lazy-path migration; because native-asset.ts is transitively imported by the backend bootstrap, Explore, balance fetch, and useMidenFaucetId, every cold start hit the eager TLA and the splash screen stayed up indefinitely on mobile. (#203)

  • [FIX][mobile] Web-layer modals are no longer covered by the native navbar pill. The Home/Activity/Browser overlay lives in its own iOS UIWindow (and analogous Android Dialog) above the WebView, so web modals couldn't z-order above it. New useHideNavbarWhileOpen hook morphs the pill off-screen while any modal is open and back in on close, with a shared open/close reference counter so concurrent modals coexist. Wired into CustomModal (covers AlertModal / ConfirmationModal / AddContactModal via ModalWithTitle), TransactionProgressModal, and RecallBlocksModal. (#203)

  • [FIX][extension] Popup and side panel no longer flash white on reopen in dark mode. public/globals.js (already loaded before the module entry on every extension HTML) now reads theme_setting from localStorage, resolves 'system' via prefers-color-scheme, and applies .dark + the dark <html> background synchronously before first paint. External script because MV3 CSP forbids inline scripts. (#203)

  • [FIX][extension] Content scripts (contentScript.js, addToWindow.js) are now built as standalone classic IIFE bundles instead of ES modules with code-split chunks/* imports. MV3 content scripts declared in manifest.json run as classic scripts, so the import statements at the top of the previous output silently failed to parse — no error on the extension card, no entry in DevTools → Sources → Content scripts, and window.midenWallet was never injected (dApps saw WalletNotReadyError). New vite.contentScripts.config.ts builds each entry separately with format: 'iife' + inlineDynamicImports: true, and stubs lib/intercom/{mobile,desktop}-adapter so the content-script bundle doesn't drag the wasm-bindgen SDK in via their transitive deps. Wired into build:extension / build:chrome / build:firefox / build:safari / test:e2e:blockchain:build.

  • [FIX][all] Encrypted-wallet-file import now restores secret keys for every imported account, not just the first. The decrypted wallet payload carries the full WalletAccount[] (with hdIndex and type per account), and Vault.spawnFromMidenClient re-derives each auth key from the mnemonic and inserts it into the new keystore via client.keystore.insert. Previously the imported miden-client DB came over without keystore entries, so signing broke for any non-default account.

  • [FIX][all] Encrypted wallet file export now includes wallet account metadata alongside the miden-client/wallet DB dumps, so import can preserve account names and HD indices instead of falling back to generic "Miden Account N" labels.

  • [FIX][all] Encrypted-file password screen consolidates the hardware-vs-password branching around a single hasHardwareProtector check — hardware-only vaults skip password entry entirely, password-protected vaults keep the attempt/lockout flow.

  • [FIX][all] Encrypted-wallet-file flow now filters imported accounts out of the exported payload and adds a red inline notice naming the count of omitted accounts, because the file format does not carry raw private keys. Restore (Vault.spawnFromMidenClient) silently skips miden-client accounts with no matching WalletAccount entry and any entry with hdIndex < 0, so a stray orphan no longer either aborts the restore or overwrites an imported account's real secret with a mnemonic-derived one under m/44'/0'/0'/-1'. (#195)

  • [FIX][all] ACCOUNT_NAME_PATTERN in app/defaults.tsx is now anchored at both ends (/^[^\s-].{0,15}$/); the prior /[^\s-].{0,16}$/ was missing the start anchor, which silently accepted names of ANY length because the regex engine could match any suffix. EditAccountName / CreateAccount / Import Account now all enforce the same 16-character ceiling. Behaviour note: existing wallets that have accounts named with 17+ characters (accepted by the old broken regex) will still DISPLAY those names fine, but the first attempt to rename one of those accounts will fail validation until the user shortens it to 16 characters or fewer. (#195)

  • [FIX][all] importAccount now serializes through the unlock queue and re-reads the accounts list inside the serialized section, so two quick successive imports can't both pass the name-uniqueness check against stale data and lose one of the writes. Auto-generated default names (Account N) also walk forward past collisions instead of throwing when the user has manually renamed an earlier account to match the template. (#195)

  • [FIX][all] Hex validation on private-key import rejects odd-length input and caps the input at 4 KiB (~1.6× the serialized Falcon-512 secret key size) before allocation, so a malformed or pathologically large paste — e.g. an accidentally-pasted seed phrase or encrypted-file body — fails fast with a clean PublicError instead of throwing inside AuthSecretKey.deserialize under the WASM lock. (#195)

  • [FIX][all] Encrypted-wallet-file restore now fails fast with a clear "Encrypted file contains no restorable accounts" error instead of crashing on an unguarded walletAccounts[0]!.publicKey dereference when the file carries zero HD accounts (e.g. a wallet whose only account was imported and therefore filtered out on export). (#195)

  • [FIX][all] Encrypted-wallet-file import mirrors the export-side warning: after successful decryption, if the file was exported with any imported accounts stripped, the restore screen now shows a red notice naming the omitted count and requires an explicit second "Continue Import" click before completing. Count is carried in a new optional omittedImportedAccountCount field on the decrypted payload; older files without the field restore silently as before. (#195)

  • [FIX][all] Reveal Private Key flow is now gated by an "I understand this key cannot be rotated" checkbox in addition to the warning banner — the Continue button stays disabled until the user ticks it, turning a passive notice into an active acknowledgment. (#195)

  • [FIX][extension] Popup no longer white-screens after an MV3 service-worker cold-start. WalletStoreProvider stopped gating the app tree on a racy single GetStateRequest, and useIntercomSync replaced its fixed 15 s retry budget + one-shot latch with a cancellable unbounded retry loop (250 ms → 3 s exponential backoff). The backend's existing post-init StateUpdated broadcast still hydrates the store as soon as the SW is ready; the popup now self-heals from a missed broadcast or slow port setup instead of staying blank until fully reopened. (#196, closes #113)

  • [FIX][extension] Service-worker init race resolved across all known Vault.* cold-start entry points. The Vite SW build's sw-patches plugin strips top-level await and re-sequences module init through a __initsReady barrier, but init_actions was excluded from that barrier and SYNC_REQUEST bypassed it — so Actions.init(), runSync(), isDAppEnabled(), and the miden-sync alarm listener could read Vault === undefined and throw TypeError: Cannot read properties of undefined (reading 'isExist'). Downstream symptoms included send/consume txs stuck in GeneratingTransaction indefinitely, "Create Account" silently no-op'ing, and dApp permission checks failing on cold-start. New getVault() lazy accessor in src/lib/miden/back/vault.ts awaits init_vault() (idempotent via a one-shot promise) before returning the class; wired through actions.ts (init, isDAppEnabled), sync-manager.ts (runSync × 2), and the alarm listener in background.ts. SYNC_REQUEST removed from the __initsReady bypass list in vite.background.config.ts. Misleading "doSync is safe before start completes" comment in background.ts:33-36 removed. Also patches a Rolldown-renaming regression: source-level init_vault() references were getting auto-renamed to init_vault$1 to avoid colliding with the auto-generated factory name; a small post-bundle alias (var init_vault = init_vault$1;) appended to the chunk keeps the lazy accessor's runtime references resolvable. Full audit of 11 Vault.* call sites included; non-cold-start sites (post-withInited/withUnlocked) verified safe by gate. (#214, closes #212)

  • [FIX][all] Encrypted-wallet-file import (and the consume-note screen) no longer crash with useMiden must be used within a MidenProvider. PR #151's React-SDK migration introduced useImportStore and useConsume calls without ever mounting the SDK's MidenProvider — a name collision with the wallet's own MidenProvider wrapper masked the missing import. src/lib/miden/front/provider.tsx now mounts MidenProvider from @miden-sdk/react/lazy (matching the lazy entry the call sites use) inside the existing provider composition, with MidenConfig derived from the same MIDEN_NETWORK_ENDPOINTS / MIDEN_PROVING_ENDPOINTS / getNoteTransportUrl constants MidenClientInterface.create() already consumes. autoSyncInterval: 0 keeps the wallet's own SW-driven sync as the source of truth. (closes #200)

  • [FIX][extension] TransactionProgressModal no longer shows "Transaction Completed" for transactions that actually failed. The modal's extension-branch polling loop only watched getAllUncompletedTransactions(), never getFailedTransactions(), and the local error state was only set on the non-extension code path — so any tx that flipped to Failed (via cancelTransaction's WASM-kernel-error catch) dropped silently out of the uncompleted list and the modal rendered the green-checkmark success branch. Brought the modal to parity with the full-page GeneratingTransaction route: a second 5s SWR poll on getFailedTransactions(), an initial-failed-count snapshot captured in a ref on first load, and hasErrors = error || (currentFailed - initialFailed) > 0 derived from the delta. The shared <GeneratingTransaction> sub-component now renders the failure branch correctly. (closes #211)

  • [FIX][all] Auto-consume retry storm bounded with per-noteId cap and cooldown. When a Committed input note hit a deterministic-looking kernel auth failure (miden::protocol::auth::request), initiateConsumeTransaction's dedup logic excluded Failed rows from its filter — combined with a 5s Explore.tsx polling cadence and a wallet-tab-switch remount amplifier, this produced 100+ Failed consume rows for a single noteId in <30 minutes. New constants in src/lib/miden/activity/transactions.ts: MAX_CONSECUTIVE_CONSUME_FAILURES = 5, RECENT_FAILURE_WINDOW_SEC = 30 min, RETRY_COOLDOWN_SEC = 5 min. The rw transaction now reads ALL consume rows for the noteId and partitions: non-Failed rows take the existing dedup branch; Failed-only rows gate on (a) cap on consecutive failures inside the recent window AND (b) cooldown since most recent. Preserves the documented "Failed → retry allowed" semantics for transient kernel failures (the reporter's followup confirms auth::request clears once chain state advances), but bounds the rate to ~1/5min after the cap. (closes #215)

  • [FIX][extension] Send / consume transactions stuck in GeneratingTransaction for hours after MV3 service-worker idle-eviction now self-heal. Three independent gaps closed: (1) setupTransactionProcessor's startup gate switched from hasQueuedTransactions() (Queued-only) to getAllUncompletedTransactions().length > 0, so an orphan in Generating is visible to SW-startup recovery — safeGenerateTransactionsLoop's first action cancelStuckTransactions() reaps it within a tick. (2) New STUCK_TX_HEAL_ALARM (5-min period) calls cancelStuckTransactions() directly, independent of startTransactionProcessing running, so the documented 30-min MAX_WAIT_BEFORE_CANCEL self-heal honours its contract regardless of UI mount state. Plus a one-shot sweep at the tail of setupTransactionProcessor for already-aged orphans. (3) requestSWTransactionProcessing() no longer silently swallows errors with .catch(() => {}) — failures are logged. (4) TransactionProgressModal's recovery effect now subscribes to intercom StateUpdated (broadcast at the tail of start(), doubles as an SW-respawn signal) and re-runs resumeIfNeeded on every respawn instead of mount-once. The auto-cascade danger flagged in the issue (orphan-recovery → cancel → broken-auth → retry storm) is bounded by #215's new per-noteId cap landing in the same change. (closes #216)


1.14.3 (TBD)

Features

  • [FEATURE][all] Per-stage label in the transaction progress modal. Each observable phase boundary (syncing, sending, confirming, delivering) writes a stage marker during tx processing, and the modal renders a stage-specific title + description instead of a single opaque "Generating Transaction" for the whole 3-8s spinner window. Send-type sub-label varies by tx type (claim / execute / send), and the batch subtitle surfaces a remaining-count when more than one tx is in flight.

1.14.2 (TBD)

Features

  • [FEATURE][all] Typed sign-callback failure recovery via SDK lastAuthError(). When the wallet gets locked mid-transaction, the transaction is left Queued for retry after unlock instead of marked Failed. (#189)
  • [FEATURE][all] ApplyTransactionAfterSubmitFailed handling via SDK errorCode dispatch. Transactions that submit on-chain but fail to apply locally are marked Completed (not Failed). (#189)
  • [FEATURE][e2e] Transport-failure perturbation (STRESS_TRANSPORT_FAIL_PROB) in the stress suite for end-to-end coverage of the SDK's durable relay outbox (miden-client#2127). (#189)

Fixes

  • [FIX][mobile] Switched all @miden-sdk/miden-sdk and @miden-sdk/react imports to the explicit /lazy subpath. Both SDKs' default entries (post-split) await WASM at module top level for ergonomic dApp use; Capacitor's capacitor://localhost scheme handler interacts poorly with that TLA and hangs the host WebView indefinitely (React tree never mounts). The /lazy entries omit the TLA, leaving readiness to MidenProvider's existing isReady flag.
  • [FIX][all] Gated page-side SDK WASM init. fetchTokenMetadata and SendDetails used to race the SDK's lazy wasm-bindgen load when constructing Endpoint/RpcClient directly on the page thread, hitting Cannot read properties of undefined (reading '__wbindgen_malloc') and blacklisting the token via autoFetchMetadataFails for the rest of the session. New ensureSdkWasmReady() helper actively triggers the SDK's loadWasm() via a Vite-aliased deep import and probes readiness, wired up before any page-side RPC construction. (#187)
  • [FIX][all] clearStorage no longer tears down live Dexie handles. The spawn-time reset used to call Repo.db.delete() + db.open(), which fired a versionchange event to every other open handle (notably the page's), forced them closed, and triggered DatabaseClosedError on subsequent page-side reads. Now clears only the transactions table; a new resetStorageDestructive() preserves the full-wipe semantics for the options-page "Reset Wallet" button that actually wants it. (#187)

1.14.0 (TBD)

Features

  • [FIX][mobile] Fixed iOS release build by removing stale CocoaPods references, using correct workspace target, fixing ExportOptions team ID, and adding auto-versioning from package.json. (#172)
  • [FEATURE][mobile] Embedded dApp browser for iOS and Android with multi-instance tabs, parked-dApp switcher tray, and native navbar overlay.
  • [FEATURE][all] Migrated backend from WasmWebClient to the new MidenClient TypeScript API. All service-worker WASM access now goes through MidenClientInterface wrapping the high-level MidenClient surface.
  • [FEATURE][all] Migrated frontend to @miden-sdk/react hooks (useMiden, useSyncState, useAccount, etc.), replacing manual sync and balance-polling logic.

Fixes

  • [FIX][all] Fixed duplicate consume-transaction entries in wallet history when receiving a single note. initiateConsumeTransaction now dedups against all non-Failed consume txs for the same note (including Completed), preventing auto-consume from re-enqueueing while getConsumableNotes is still returning the note during chain-sync lag. Also replaced the sync poll's blanket clear of extensionClaimingNoteIds with a surgical remove so Explore's isBeingClaimed gate works correctly. (#184)

1.13.3 (2026-03-19)

Features

  • [FEATURE][arch][all] Moved to service-worker-first architecture. The WASM client now lives exclusively in the Chrome extension service worker, with the frontend communicating via intercom messaging. Eliminates duplicate WASM instances and fixes concurrency panics.
  • [FEATURE][all] Complete UI revamp with new design system, updated layouts, and refreshed components across all screens.

1.13.2 (2026-03-16)

Features

  • [FEATURE][extension] Chrome Side Panel mode with popup toggle. Users can switch between popup (default) and side panel via the maximize/minimize icon in the header. Preference persists across sessions. (#176)
  • [FEATURE][extension] Pin extension prompt shown once after fresh install, guiding users to pin the extension to the toolbar. (#176)
  • [FEATURE][all] Color-coded Send (blue) and Receive (green) action buttons on the home page, matching the token detail page. (#176)

Fixes

  • [FIX][extension] Fixed onInstalled event handler not firing on Chrome MV3 due to webpack async module loading delaying listener registration. Handler moved to sw.js for synchronous registration. (#176)
  • [FIX][all] Fixed ConsumingNote page using raw UA sniffing instead of isMobile() platform detection. (#176)
  • [FIX][all] Fixed transaction recovery after network outages. Private accounts could enter a permanently broken state where all transactions fail with "initial state commitment does not match". Root causes: AutoSync loop died on the generating-transaction page, transactions were built against stale local state, and the transaction modal blocked on stale tx failures. Now syncs state before executing transactions, keeps AutoSync alive during transaction generation, cancels crashed/stale transactions properly, and shows correct "Failed" status instead of misleading "Executing". (#150)
  • [FIX][all] Removed stale "Download Generated Files" button and output notes storage. The useExportNotes hook, registerOutputNote, and related storage key were unused dead code. Simplifies the transaction completion screen and its auto-close logic. (#160)
  • [FIX][all] Removed the "Upload File" button and drag-and-drop note import from the Receive page. The freed space is now used by the notes list, making it taller. (#161)
  • [FEATURE][all] Complete UI revamp across the wallet.

1.13.1 (2026-02-16)

Features

  • [FEATURE][all] Token metadata now fetched via RpcClient instead of IndexedDB lookups, improving reliability and reducing stale metadata issues. (#127)

Fixes

  • [FIX][extension] dApp-initiated transactions (e.g. from wallet adapter) now process in the background instead of requiring the user to keep the popup open. Previously, closing the extension popup during a dApp transaction could cause it to fail silently. (#130)
  • [FIX][all] Fixed Note Transport Layer (NTL) connection failures caused by incorrect default port configuration. Updated faucet address to use the new testnet faucet. (#125)

1.13.0 (2026-02-12)

Breaking Changes

  • [BREAKING][rename][all] Miden SDK package renamed from @demox-labs/miden-sdk to @miden-sdk/miden-sdk and upgraded to v0.13.0. All imports and wallet adapter references updated accordingly. (#101)

Features

  • [FEATURE][mobile] Mobile app for iOS and Android via Capacitor. Includes FaceID/TouchID biometric authentication, in-app dApp browser with wallet adapter injection, QR code scanning and display, native local notifications for incoming notes, haptic feedback throughout the UI, hardware back button and swipe-back gesture support, and native file sharing for exports. (#81)
  • [FEATURE][desktop] Desktop application using Tauri for macOS and Windows. Features native window controls, Touch ID unlock on macOS, and secure storage via Tauri's stronghold plugin. (#86)
  • [FEATURE][desktop] dApp browser for desktop with dedicated browser window that injects window.midenWallet API into web pages. dApps can request wallet connections and transaction approvals via an in-window confirmation overlay. (#89)
  • [FEATURE][mobile,desktop] Hardware-first vault key security. On devices with Secure Enclave, TPM, or TEE, the wallet now uses hardware-only protection with no password required. Eliminates password brute-force attack surface on mobile and desktop. Browser extension continues to use password-based protection. (#88)
  • [FEATURE][all] Note Transport Layer (NTL) support. Enables private note delivery between wallets using encrypted peer-to-peer transport, allowing users to send and receive notes without exposing transaction details on-chain. (#45)
  • [FEATURE][all] Runtime language switching with Spanish and Polish support. Language can be changed instantly in Settings without page reload. Unified i18n system using i18next exclusively, replacing the legacy custom T/t components. Updated branding from Demox Labs to Miden across the app. (#74)
  • [FEATURE][all] Transaction completion tracking. Added waitForTransactionCompletion using Dexie's liveQuery so the UI can reliably wait for transactions to finalize rather than just queuing them. (#50)
  • [FEATURE][all] Improved seed phrase verification during onboarding. Users now verify the first and last words of their seed phrase (similar to Coinbase Wallet) instead of always the 10th word, which caused confusion when users hadn't scrolled far enough during backup. (#51)
  • [FEATURE][all] Receive page overhaul with "Claim All" button. Users can claim all pending notes in a single action. Claiming state persists across popup reopens, and new notes arriving during a claim show the button again. Includes error handling with retry support. (#55)
  • [FEATURE][all] Improved balance sync UX. Balances default to 0 immediately on page load instead of showing a skeleton loader. A shimmer animation indicates sync progress on each token row, disappearing after the first chain sync completes. (#65)
  • [FEATURE][all] Subtle header spinner replaces the wave loading animation for balance syncing, providing a less intrusive loading indication. (#100)
  • [FEATURE][all] i18n enforcement in CI. All user-facing strings are now required to use translation keys, enforced by a linting rule. Prevents hardcoded English strings from being introduced. (#71)

Fixes

  • [FIX][all] WASM client concurrency fix. Removed all Promise.all usage with the WASM client to prevent "recursive use of an object" panics when multiple operations (e.g. fetching metadata for 2+ new tokens simultaneously) tried to access the client concurrently. (#53)
  • [FIX][all] Fixed non-MIDEN tokens appearing delayed after sync. Previously, fetchBalances() released the WASM lock between getAccount() and metadata fetches, allowing AutoSync to grab the lock for 30+ seconds. All WASM operations now happen in a single lock acquisition. (#98)
  • [FIX][all] Wallet now syncs state to chain tip before creating the first account during onboarding, resulting in faster initial setup. (#99)
  • [FIX][all] Fixed autosync lock interfering with balance display. Balances are now fetched immediately when wallet becomes Ready (moved from React useEffect to syncFromBackend in Zustand store), eliminating a ~200ms delay. (#84)
  • [FIX][all] Fixed generating transaction page showing success icon and "transaction complete" text even when the transaction failed. Error state now displays correctly with appropriate messaging. (#48)
  • [FIX][all] Fixed custom transactions (e.g. from dApp swaps) not appearing on the activity/history page. The issue was that custom transactions don't have the note tag attached to the address. (#70)
  • [FIX][all] Fixed undefined note metadata causing errors when handling private notes with request metadata. (#73)
  • [FIX][extension] Fixed onboarding flow reopening in a new tab when the popup was clicked while onboarding was already in progress. The extension now redirects to the existing onboarding tab instead. (#52)
  • [FIX][extension] Replaced the tab-opening pattern with an in-popup modal for transaction progress, unifying the UX across mobile, desktop, and extension. (#87)
  • [FIX][extension] Disabled all CSS transitions and animations in the Chrome extension to prevent visual glitches. Fixed isExtension() detection to check both browser.runtime.id and chrome.runtime.id. (#83)
  • [FIX][extension] Fixed receive page content overflowing the popup viewport. (#47)
  • [FIX][extension] Fixed settings page bottom toolbar being cut off. (#75)
  • [FIX][all] Fixed form fields (send flow, encrypted file export, etc.) broken after the react-hook-form v7 migration. Updated all FormField components to use the new register/validation API. (#66)
  • [FIX][all] Fixed stability issues on the Consuming Note page that could cause errors during note consumption.
  • [FIX][desktop] Fixed Windows desktop app icon displaying incorrectly. The icon.ico file was a PNG renamed to .ico; converted to proper ICO format required by the Windows Resource Compiler. (#93)
  • [FIX][all] Resolved 63 dependency security vulnerabilities (66 down to 3). Removed react-dev-utils (3 critical), node-forge (4 high), replaced analytics-node with @segment/analytics-node, and updated @svgr/webpack, nanoid, and translate to patched versions. (#96)