- [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.
- [FIX][all] Activating a device key on a migrated Guardian account no longer permanently breaks its sync. After a structural rotation (
update_signersfor replace-hot-key, or the follow-upupdate_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 forswitch_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 onensureSafeToOverwriteLocalState— 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.
syncGuardianAccountsnow only syncs Guardian accounts that actually carry a hot key, instead of every account not flaggedrequiresHotKeyRotation. 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, sogetOrCreateMultisigServicethrew 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 areplace-hot-keyorswitch-guardiantransaction 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 ConflictPendingDeltawhile 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 transient409(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
.privateKeyUsageonly — 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)..userPresenceis used rather than.biometryCurrentSetso 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? SecKeydowncasts 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 aCFGetTypeID(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-clientand its procedures live in theopenzeppelin::auth::*MASM namespace, so they don't MAST-match any bundledmiden-standardstemplate. The SDK'sAccountInterfacetherefore classifies the component asCustomandAccount.getPublicKeyCommitments()returns[]; the wallet's connect flow read that as "no public key" and rejected the connection (surfaced to the dApp asNOT_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'sopenzeppelin::multisig::signer_public_keysstorage map — the key the wallet actually signs with — so Guardian accounts resolve a usable session key. Plain single-key accounts are unaffected (theirAuthSingleSigcomponent 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/evaluatenow race the WebKitexecuteAtomcall against a 30s hard timeout (matchingevalAsync), so a wedged RWI socket or a momentarily-blocked WebView main thread surfaces as a fast throw instead of an indefinite await. PreviouslypollForConditioncould only check its deadline between iterations, so a single hungevalstalled 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--retriesrestarts on a fresh app + CDP. (#302) - [CHANGE][ci] Blockchain E2E retries the
miden-clientharness 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 oneisTransientCliErrorhelper. (#302) - [CHANGE][ci] iOS E2E mobile jobs run on dedicated
macos-26-xlargerunners, and are more resilient to degraded shared runners. The shared standardmacos-26runner pool was intermittently degraded for hours at a time by noisy-neighbour IO contention — everysimctlop crawled (97 CI samples: per-wallet_simPairsetup 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-xlargelarger 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_simPairfixture 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 recoverysimctl shutdown alls to clear half-booted device state (theSimError 405signature); 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_structureassertion's__TEST_GUARDIAN_AUTH__hook used to build aMultisigService(getOrCreateMultisigService→MultisigClient.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 (~48signWithHotKeycalls 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 withAccountInspector.fromAccount— a pure parse with no signing, no guardian HTTP, and no client load (just onegetAccount, the same read the balance poll already does). Because even that lonegetAccountwas 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 SYNCHRONOUSexecute_scriptatom (polled), not the asyncexecute_async_scriptone: appium-remote-debugger's async atom delivers its completion callback in thearguments[arguments.length-1]slot as the booleantrueon this iOS RWI bridge, socb(result)threwTypeError: cb is not a function, the promise rejected unhandled, and everyevalAsynchung 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 onMIDEN_E2E_TESTand tree-shaken from production. (#302)
- [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
TransactionRequestfrom the Guardian's advice. (#153)
- [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 to0.15.0-rc.0(built against@miden-sdk/miden-sdk ^0.15.0), replacing the published0.14.9— which targets SDK0.14.5and trapped withRuntimeError: memory access out of boundsinsideMultisigClient.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-sdkand@miden-sdk/reactfrom0.15.1to0.15.2(and the matching**/@miden-sdk/miden-sdkresolution pin).@miden-sdk/vite-pluginstays at0.14.11. (#153)
- [FIX][all] Corrected the devnet Guardian endpoint host (
stg-guardian.openzeppelin.com→guardian-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-clientimports the eager@miden-sdk/miden-sdkentry; 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/lazyalias, restoring the Chrome package to ~18 MB (was ~29 MB).
- [CHANGE][all] Bumped
@miden-sdk/miden-sdkand@miden-sdk/reactto the stable0.15.1release, replacing the0.15.0-alpha.7prerelease (npmnextdist-tag) that shipped in 1.15.0 with the GA build on thelatestdist-tag.@miden-sdk/vite-pluginstays at0.14.11(no 0.15.x is published; it's SDK-version-agnostic), and the**/@miden-sdk/miden-sdkresolution pin tracks the same0.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 approachbuild-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 failingMobile E2Eat 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 viasimctl.
- [FEATURE][all] 3-key Guardian: cold co-signs
switch_guardian(on-chain threshold-2 satisfied viaprocedureThresholds) and a new Settings → Rotate Device Key flow lets users replace the hot signer in-place via a single cold-signedupdate_signersproposal, persisting the new ciphertext before submission and finalizingWalletAccount.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.recoverByKeyper HD index until first miss); each adopted account is flaggedrequiresHotKeyRotationand the home view surfaces an Activate Device Key banner with a one-click CTA that fires the same cold-signedupdate_signersrotation — the banner self-hides onceVault.swapHotKeylands. TheswapHotKeymessage now carries onlynewHotPubKey; the vault resolves the previous hot from the persistedWalletAccount, 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-keyfacade — extension/desktop use a JS fallback; iOS wraps the secret under a per-account Secure Enclave P-256 key viaHotKeyPlugin.swift; Android wraps it under a per-account StrongBox-backed RSA-OAEP key viaHotKeyPlugin.ktwith 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 forupdate_signers/update_guardian/update_procedure_thresholdis enforced client-side per the Phase 0 SDK reading. Storage addsaccColdSecretKeyStrgKeyandWalletAccount.{hotPublicKey,coldPublicKey}so role-awaresignWord(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 optionalauthSchemefield onWalletAccount(legacy entries with no field default to Falcon, matching the historical default 1:1). Private-key import detects the scheme from the deserializedAuthSecretKeyvia the SDK's per-scheme accessor. New accounts created post-upgrade get ECDSA stamped into theirWalletAccountrecord. Encrypted-file format change is purely additive — old files round-trip through restore as Falcon. (#229)
- [CHANGE][all] Migrated to the Miden 0.15 protocol line —
@miden-sdk/miden-sdkand@miden-sdk/reactbumped to the0.15.0-alphaseries (npmnextdist-tag;@miden-sdk/vite-pluginstays at0.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.fromAccountreads the new metadata slot); their assets now display as "Unknown" instead of being blacklisted for the session. - The dApp
ImportPrivateNoteResponse.noteIdfor details-only note imports (the commonnoteBytespath) now carries the note's details-commitment hex rather than a note-id hex, followingnotes.import's new return contract. - Network/fee-asset discovery follows the protocol rename (
BlockHeader.feeFaucetId(), formerlynativeAssetId()); the discovery cache is keyedv2so 0.14-cached IDs are not reused. - Account storage modes are
public/privateonly (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-clifrom a pinned git rev (midenClientCliGitin 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.
- [FIX][all] Guardian sync no longer leaks per-init web-client workers.
MultisigService.initnow reuses the sharedgetMidenClient()singleton instead of a throwawayMidenClientInterface.create({})client, andgetOrCreateMultisigServicecoalesces 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.7restores 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-sdk≥0.15.0-alpha.6(the explicit-prover fix in web-sdk#182). Root-cause analysis in web-sdk#180.
- [FIX][extension] Local proving (
chrome.offscreenMT-WASM path) no longer hangs the wallet. Everyconsumeandsendwithdelegate_proof_setting_key=falsewould deadlock inside the SDK's_withInnerWebClientcallback atinner.getInputNote(...)— the callback's outer_serializeWasmCallslot awaited the inner_serializeWasmCallslot that was queued behind it (classic re-entrant-lock deadlock; see web-sdk#152 for the SDK-side root cause + fix). Downstream, the wallet'sSyncManagersaw 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/newTransactioncomplete normally on the offscreen-doc prove path. Requires@miden-sdk/miden-sdk@0.14.10. - [FIX][extension]
SyncManagerno longer surfaces a "cannot reach the miden node" banner during legitimate long local proves. Previously the 5 sSYNC_TIMEOUT_MSbounded 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 andmarkConnectivityIssue('node'). The timeout now wraps the RPC alone; queued syncs wait patiently behind the prove (which yields the lock viayieldWasmClientLockaround 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
TransactionProgressModalauto-dismiss was correctly gated on terminal-state signals so the "Tx Completed → View on Midenscan" screen wouldn't be ripped away when the success-pathnavigate('/')fires after a long local prove — but with no other dismissal path the modal stayed up as a full-viewportzIndex: 9999overlay 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 sawlocator.clicktime out againstgetByTestId('send-flow').locator('div.cursor-pointer')in 80–99.5% of attempts.SendManagernow closes any stale completion modal on entry — an explicit "starting a new tx" signal equivalent to tapping Done. Initially shipped in #245 gated onlastCompletedTxHash !== 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 thelastCompletedTxHashgate — every open completion modal closes on send-flow entry, covering theReceive/ConfirmPage/SendManagermodal-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_MSwas 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 (withTimeoutonly rejects the outer promise; the underlyingsyncStatekeeps running and holding the lock until it settles), so the aggressive ceiling bought nothing and only manufactured false failures; (2)markConnectivityIssueran 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_MSis raised to 30s (a true wedged-sync watchdog), and the connectivity banner is now gated on the sameMAX_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.
- [CHANGE][all] Bumped the Miden SDK to
0.14.11—@miden-sdk/miden-sdk,@miden-sdk/react, and@miden-sdk/vite-pluginare kept in lockstep (with the**/@miden-sdk/miden-sdkresolution pin) to avoid mismatched-version WASM/type issues.
-
[FIX][extension] A failed public-key fetch during dApp connect no longer leaves the wallet permanently stuck on "Connecting…". In the extension connect path (
generatePromisifyRequestPermissioninsrc/lib/miden/back/dapp.ts), ifgetAccountPublicKeyB64threw (e.g. the account was momentarily unavailable during instability), the error was swallowed and a dApp session was persisted withpublicKey: null. The direct-return path then handed that null public key back verbatim on every subsequent connect — with no confirmation popup — so the dApp'sconnect()never resolved and the only escape was clearing the session or reinstalling. The extension branch now fails closed (rejects withNotGranted, 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#7286A0ring — overlapping them.'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 orangelogo-white-bgfavicon, so a devnet wallet opened in a browser tab showed the orange production logo. The extension build'stransformIndexHtmlnow swaps the favicon tologo-devnetforMIDEN_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.fidomain in two places — the onboarding password screen (CreatePassword) and Settings → About — and the Settings → About entries additionally rendered<a href="#">because they usedslug: '#'. NewPRIVACY_POLICY_URL/TERMS_OF_USE_URLconstants insrc/app/constants.tspoint at the livehttps://0xmiden.github.io/wallet/privacy/. There is no dedicated Terms page yet, soTERMS_OF_USE_URLtemporarily 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
PageLayoutclose button passed a literalfill="black"to itsIcon, 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 passesfill="currentColor", inheriting the button'stext-blacktoken (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 byTransactionIcon) hardcodedfill="black", so it was invisible on the dark app background. It now usesfill="currentColor"driven by atext-blackclass (var(--color-text-primary)), rendering black in light and white in dark — matching the app's othercurrentColoricons. The sharedLoader(components/Loader.tsx) also defaultedcolorto'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#E5E7EBtovar(--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 bydarkMode: 'class'+ CSS custom properties insrc/main.css, but these hardcoded utilities never flipped — so in dark mode light-gray captions vanished, inputs/cards/modals stayed white, hairline borders disappeared, andNavigator's page-transitionbackgroundColorpainted every routed page#ffffff. All such sites now use the var-backed semantic tokens that flip with the.darkclass (text-text-muted,text-heading-gray,border-border-card/-light,bg-surface-solid/-chip-bg/-input-bg,bg-gray-25/50/100);Navigatorreadsvar(--color-app-bg); and pastel status alerts gaineddark:fill/text variants (using valid-500text shades, since this palette has nored/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:mobileclean; generated utilities map tovar(--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
requestConsumemay carry serialized note bytes viaConsumeTransaction'snoteBytesparameter, which the wallet enqueues (queueNoteImport) and later imports viaNoteFile.deserializeinimportAllNotes(src/lib/miden/activity/notes.ts). That parameter has an undocumented, unvalidated requirement: it must be a serializedNoteFile. The natural thing for a dApp to pass —note.serialize()— produces a serializedNoteinstead (a different container, with no"note"magic header), so deserialization throwsnotefile 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 aNotehere, so this specific mismatch no longer fails at all.) PreviouslyimportAllNotesaborted 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. BecauseimportAllNotes()is the first call ingenerateTransactionsLoop, 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.importAllNotesnow 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 afterMAX_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. aNoteFile::NoteIdimport, 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 beforesyncState, 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 withoutimportAllNotesever throwing, the queue draining even whensyncStatethrows, and concurrently-enqueued notes preserved across a pass. Reported in0xMiden/wallet-adapter#87. -
[FIX][all] Importing note bytes now accepts a serialized
Note, not just aNoteFile.MidenClientInterface.importNoteBytes(src/lib/miden/sdk/miden-client-interface.ts) previously calledNoteFile.deserializedirectly, so any caller passing a serializedNote— the natural output ofnote.serialize(), and what a dApp typically has on hand forConsumeTransaction's undocumentednoteBytesparameter — failed withnotefile deserialization failed: invalid utf-8 sequence.... It now accepts either form: aNoteFileis used directly, and a bareNoteis wrapped into aNoteFile(theNoteDetailsvariant, mirroringNoteFile.fromInputNotewhen 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 behind0xMiden/wallet-adapter#87: combined with the brick fix above, a dApp consuming a note viarequestConsumewithnote.serialize()bytes now works end to end. Covers the consume path, custom-transactionimportNotes, and the extension's import-note request, since all route throughimportNoteBytes. Reported in0xMiden/wallet-adapter#87. -
[FIX][dapp] Generalized
requestTransactionnow handlessend/consume, not justcustom. A dApp consuming a note through the typedTransaction(TransactionType.Consume, new ConsumeTransaction(...))API (rather than the dedicatedrequestConsume) hitINVALID_PARAMS: Invalid CustomTransaction payload: the wallet'srequestTransactionhandler (generatePromisifyTransactioninsrc/lib/miden/back/dapp.ts) readreq.transaction.payloadand validated it as aMidenCustomTransactionunconditionally, ignoring the taggedMidenTransaction.type. It now dispatches bytype—senddelegates to the send flow (generatePromisifySendTransaction) andconsumeto the consume flow (generatePromisifyConsumeTransaction), reusing their existing preview / confirmation / execution paths, whilecustom(and bare/legacy payloads) keep flowing through the custom path. Thetypediscriminant is compared by its wire string value rather than importing the adapter-baseTransactionTypeenum (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()insrc/lib/miden/back/dapp.tsused a non-null assertion (NETWORKS.find(n => n.id === net)!.rpcBaseURL) — when a dApp calledconnect()without passing anetworkargument (which the testnet faucet, and any dApp using the standard@demox-labs/miden-wallet-adapter-baseAPI, does — they just want to use whichever network the wallet is on), thefind()returned undefined and the wallet threwCannot read properties of undefined (reading 'rpcBaseURL')immediately. The error was wrapped into aMIDEN_PAGE_ERROR_RESPONSEand shipped back to the dApp before the permission modal ever rendered.getNetworkRPCnow falls back togetCurrentMidenNetwork()whennetis undefined and throws a cleanNetworkNotGrantedfor genuinely unknown ids. Second crash, hidden behind the first:DappBrowserProviderwas passingshortAccountId(the truncatedmtst1apsnk6...qq9wr6wdisplay string with literal...) intoDappConfirmationModal. The modal echoed that truncated string back asaccountPublicKeyon Approve. The backend then calledmidenClient.getAccount(accountId)with the truncated form, the WASM bech32 decoder threwinvalid character (code=.), andrequestPermissionmapped it toNOT_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 viatruncateAddress(), and sends the full id back throughonResolve. End-to-end CDP repro on Android emulator confirmsconnect()now resolves with{accountId, publicKey, privateDataPermission, allowedPrivateData}in ~430 ms. Regression tests cover both:getNetworkRPC(undefined)returns the current network's RPC,getNetworkRPC('unknown')throwsNetworkNotGranted, 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
TransactionProgressModalauto-dismiss was correctly gated on terminal-state signals so the "Tx Completed → View on Midenscan" screen wouldn't be ripped away when the success-pathnavigate('/')fires after a long local prove — but with no other dismissal path the modal stayed up as a full-viewportzIndex: 9999overlay 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 sawlocator.clicktime out againstgetByTestId('send-flow').locator('div.cursor-pointer')in 80–99.5% of attempts.SendManagernow closes any stale completion modal on entry — an explicit "starting a new tx" signal equivalent to tapping Done. Initially shipped in #245 gated onlastCompletedTxHash !== 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 thelastCompletedTxHashgate — every open completion modal closes on send-flow entry, covering theReceive/ConfirmPage/SendManagermodal-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][mobile] Mobile sync no longer hangs on WKWebView. The SDK's Web Worker shim runs every
client.*call (includingsyncState()) 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, leavingisSyncing: trueandlastSyncedAtfrozen, with newly-minted public notes never surfacing in the Receive screen. Wallet now passesuseWorker: falsetoMidenClient.createon mobile (the option lands in@miden-sdk/miden-sdk@0.14.9from web-sdk#149), so the SDK runs in the main thread on mobile (mirroring the pre-0.14.4 behavior). Desktop / Chrome extension keepuseWorker: true(the Worker shim is fine there). Web SDK PR: #149.
-
[FEATURE][mobile] Native Rust transaction prover on iOS + Android. On opt-in (Settings → General → Local proving), the wallet hands the SDK a
CallbackProverwhose closure routes prove calls through a new Capacitor plugin (@miden/native-prover) → JNI / Swift FFI →LocalTransactionProverlinked as a static / shared lib. Same wire bytes asRemoteTransactionProver, 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_PROOFstaystrue, the native path is opt-in, and the Chrome extension is unaffected (worker shim stays enabled, noCallbackProveris constructed). Requires web-sdk's newTransactionProver.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. See0xMiden/protocol#2906for the underlyingmiden-tx 0.14.6regression (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-sdkor@miden-sdk/reactchange can now includeWeb SDK PR: #Nin the description. CI's new.github/actions/inject-linked-web-sdk-praction clones the linked web-sdk PR, builds the SDK packages from source, and rewritespackage.jsonto consume them viafile:deps — so the wallet PR's CI builds against the actual upstream change without requiring it to publish first. A separatecheck-linked-web-sdk-pr.ymlworkflow keeps alinked-web-sdk-pr-readycommit 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 viascripts/dev-with-web-sdk-pr.sh(with--clear); a newlefthook.ymlpre-commit hook blocks committing the patched state. Mirrors web-sdk's existingClient PR: #Npattern. 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 achrome.offscreendocument hosting wasm-bindgen-rayon overnavigator.hardwareConcurrencythreads (~5-10s). The SW orchestrates execute → offscreen prove → submit → apply, releasing the WASM-client mutex during the prove via a newyieldWasmClientLockso background sync stays responsive across the multi-second prove window. A newSpeculationManagerkicks 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-timeMIDEN_USE_OFFSCREEN_PROVING=trueandMIDEN_USE_SPECULATIVE_PROVING=trueenable it (defaulted on for desktop Chrome builds viabuild:chrome). Mobile keeps remote proving — there's no equivalentchrome.offscreenAPI on mobile platforms, and the mobile build pins both flags tofalse. -
[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 existingkeystore.insert→insertKeyCallbackpath, so the reveal/sign pipeline treats imported accounts identically to HD-derived ones. Imported accounts are taggedhdIndex: -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
InAppBrowseroverlay so dismissing the overlay returns the user to the completion screen with no state loss. URL resolved per-network via a newMIDEN_EXPLORER_ENDPOINTSmap (testnet / devnet); localnet has no explorer → button hidden. The on-chain tx hash is plumbed throughSendManager.onSubmit→lastCompletedTxHashin 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)
-
[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 (lastCompletedTxHashset,transactionCompletetrue, orhasErrorstrue); 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-issuesflag (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 inlib/miden/activity/connectivity-state.tstrackingnetwork(user offline),node(Miden RPC unreachable),prover(remote prover down), andresolving(recovery probe in flight) independently. Categories auto-clear on the next successful operation of the relevant kind:proverclears on every successfulwithProverFallbackinvocation;network/node/resolvingclear on every successful sync from both the SW (sync-manager.doSync) and the in-process mobile/desktop sync loop (useSyncTrigger). The banner inExplorenow 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 deadsendConnectivityIssueruntime-message path was retired (theExtensionMessageListeneris 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.tsfrom@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; becausenative-asset.tsis transitively imported by the backend bootstrap, Explore, balance fetch, anduseMidenFaucetId, 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 AndroidDialog) above the WebView, so web modals couldn't z-order above it. NewuseHideNavbarWhileOpenhook 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 intoCustomModal(coversAlertModal/ConfirmationModal/AddContactModalviaModalWithTitle),TransactionProgressModal, andRecallBlocksModal. (#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 readstheme_settingfromlocalStorage, resolves'system'viaprefers-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-splitchunks/*imports. MV3 content scripts declared inmanifest.jsonrun as classic scripts, so theimportstatements at the top of the previous output silently failed to parse — no error on the extension card, no entry in DevTools → Sources → Content scripts, andwindow.midenWalletwas never injected (dApps sawWalletNotReadyError). Newvite.contentScripts.config.tsbuilds each entry separately withformat: 'iife'+inlineDynamicImports: true, and stubslib/intercom/{mobile,desktop}-adapterso the content-script bundle doesn't drag the wasm-bindgen SDK in via their transitive deps. Wired intobuild: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[](withhdIndexandtypeper account), andVault.spawnFromMidenClientre-derives each auth key from the mnemonic and inserts it into the new keystore viaclient.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
hasHardwareProtectorcheck — 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 matchingWalletAccountentry and any entry withhdIndex < 0, so a stray orphan no longer either aborts the restore or overwrites an imported account's real secret with a mnemonic-derived one underm/44'/0'/0'/-1'. (#195) -
[FIX][all]
ACCOUNT_NAME_PATTERNinapp/defaults.tsxis 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]
importAccountnow 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
PublicErrorinstead of throwing insideAuthSecretKey.deserializeunder 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 unguardedwalletAccounts[0]!.publicKeydereference 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
omittedImportedAccountCountfield 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.
WalletStoreProviderstopped gating the app tree on a racy singleGetStateRequest, anduseIntercomSyncreplaced 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-initStateUpdatedbroadcast 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'ssw-patchesplugin strips top-levelawaitand re-sequences module init through a__initsReadybarrier, butinit_actionswas excluded from that barrier andSYNC_REQUESTbypassed it — soActions.init(),runSync(),isDAppEnabled(), and themiden-syncalarm listener could readVault === undefinedand throwTypeError: Cannot read properties of undefined (reading 'isExist'). Downstream symptoms included send/consume txs stuck inGeneratingTransactionindefinitely, "Create Account" silently no-op'ing, and dApp permission checks failing on cold-start. NewgetVault()lazy accessor insrc/lib/miden/back/vault.tsawaitsinit_vault()(idempotent via a one-shot promise) before returning the class; wired throughactions.ts(init,isDAppEnabled),sync-manager.ts(runSync× 2), and the alarm listener inbackground.ts.SYNC_REQUESTremoved from the__initsReadybypass list invite.background.config.ts. Misleading "doSync is safe before start completes" comment inbackground.ts:33-36removed. Also patches a Rolldown-renaming regression: source-levelinit_vault()references were getting auto-renamed toinit_vault$1to 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 11Vault.*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 introduceduseImportStoreanduseConsumecalls without ever mounting the SDK'sMidenProvider— a name collision with the wallet's ownMidenProviderwrapper masked the missing import.src/lib/miden/front/provider.tsxnow mountsMidenProviderfrom@miden-sdk/react/lazy(matching the lazy entry the call sites use) inside the existing provider composition, withMidenConfigderived from the sameMIDEN_NETWORK_ENDPOINTS/MIDEN_PROVING_ENDPOINTS/getNoteTransportUrlconstantsMidenClientInterface.create()already consumes.autoSyncInterval: 0keeps the wallet's own SW-driven sync as the source of truth. (closes #200) -
[FIX][extension]
TransactionProgressModalno longer shows "Transaction Completed" for transactions that actually failed. The modal's extension-branch polling loop only watchedgetAllUncompletedTransactions(), nevergetFailedTransactions(), and the localerrorstate was only set on the non-extension code path — so any tx that flipped toFailed(viacancelTransaction'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-pageGeneratingTransactionroute: a second 5s SWR poll ongetFailedTransactions(), an initial-failed-count snapshot captured in a ref on first load, andhasErrors = error || (currentFailed - initialFailed) > 0derived 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 excludedFailedrows from its filter — combined with a 5sExplore.tsxpolling cadence and a wallet-tab-switch remount amplifier, this produced 100+ Failed consume rows for a single noteId in <30 minutes. New constants insrc/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 confirmsauth::requestclears once chain state advances), but bounds the rate to ~1/5min after the cap. (closes #215) -
[FIX][extension] Send / consume transactions stuck in
GeneratingTransactionfor hours after MV3 service-worker idle-eviction now self-heal. Three independent gaps closed: (1)setupTransactionProcessor's startup gate switched fromhasQueuedTransactions()(Queued-only) togetAllUncompletedTransactions().length > 0, so an orphan inGeneratingis visible to SW-startup recovery —safeGenerateTransactionsLoop's first actioncancelStuckTransactions()reaps it within a tick. (2) NewSTUCK_TX_HEAL_ALARM(5-min period) callscancelStuckTransactions()directly, independent ofstartTransactionProcessingrunning, so the documented 30-minMAX_WAIT_BEFORE_CANCELself-heal honours its contract regardless of UI mount state. Plus a one-shot sweep at the tail ofsetupTransactionProcessorfor already-aged orphans. (3)requestSWTransactionProcessing()no longer silently swallows errors with.catch(() => {})— failures are logged. (4)TransactionProgressModal's recovery effect now subscribes to intercomStateUpdated(broadcast at the tail ofstart(), doubles as an SW-respawn signal) and re-runsresumeIfNeededon 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)
- [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.
- [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]
ApplyTransactionAfterSubmitFailedhandling via SDKerrorCodedispatch. 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)
- [FIX][mobile] Switched all
@miden-sdk/miden-sdkand@miden-sdk/reactimports to the explicit/lazysubpath. Both SDKs' default entries (post-split) await WASM at module top level for ergonomic dApp use; Capacitor'scapacitor://localhostscheme handler interacts poorly with that TLA and hangs the host WebView indefinitely (React tree never mounts). The/lazyentries omit the TLA, leaving readiness toMidenProvider's existingisReadyflag. - [FIX][all] Gated page-side SDK WASM init.
fetchTokenMetadataandSendDetailsused to race the SDK's lazy wasm-bindgen load when constructingEndpoint/RpcClientdirectly on the page thread, hittingCannot read properties of undefined (reading '__wbindgen_malloc')and blacklisting the token viaautoFetchMetadataFailsfor the rest of the session. NewensureSdkWasmReady()helper actively triggers the SDK'sloadWasm()via a Vite-aliased deep import and probes readiness, wired up before any page-side RPC construction. (#187) - [FIX][all]
clearStorageno longer tears down live Dexie handles. The spawn-time reset used to callRepo.db.delete() + db.open(), which fired aversionchangeevent to every other open handle (notably the page's), forced them closed, and triggeredDatabaseClosedErroron subsequent page-side reads. Now clears only the transactions table; a newresetStorageDestructive()preserves the full-wipe semantics for the options-page "Reset Wallet" button that actually wants it. (#187)
- [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
WasmWebClientto the newMidenClientTypeScript API. All service-worker WASM access now goes throughMidenClientInterfacewrapping the high-levelMidenClientsurface. - [FEATURE][all] Migrated frontend to
@miden-sdk/reacthooks (useMiden,useSyncState,useAccount, etc.), replacing manual sync and balance-polling logic.
- [FIX][all] Fixed duplicate consume-transaction entries in wallet history when receiving a single note.
initiateConsumeTransactionnow dedups against all non-Failedconsume txs for the same note (includingCompleted), preventing auto-consume from re-enqueueing whilegetConsumableNotesis still returning the note during chain-sync lag. Also replaced the sync poll's blanket clear ofextensionClaimingNoteIdswith a surgical remove so Explore'sisBeingClaimedgate works correctly. (#184)
- [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.
- [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)
- [FIX][extension] Fixed
onInstalledevent handler not firing on Chrome MV3 due to webpack async module loading delaying listener registration. Handler moved tosw.jsfor synchronous registration. (#176) - [FIX][all] Fixed
ConsumingNotepage using raw UA sniffing instead ofisMobile()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
useExportNoteshook,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.
- [FEATURE][all] Token metadata now fetched via
RpcClientinstead of IndexedDB lookups, improving reliability and reducing stale metadata issues. (#127)
- [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)
- [BREAKING][rename][all] Miden SDK package renamed from
@demox-labs/miden-sdkto@miden-sdk/miden-sdkand upgraded to v0.13.0. All imports and wallet adapter references updated accordingly. (#101)
- [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.midenWalletAPI 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/tcomponents. Updated branding from Demox Labs to Miden across the app. (#74) - [FEATURE][all] Transaction completion tracking. Added
waitForTransactionCompletionusing Dexie'sliveQueryso 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)
- [FIX][all] WASM client concurrency fix. Removed all
Promise.allusage 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 betweengetAccount()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
useEffecttosyncFromBackendin 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 bothbrowser.runtime.idandchrome.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.icofile 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), replacedanalytics-nodewith@segment/analytics-node, and updated@svgr/webpack,nanoid, andtranslateto patched versions. (#96)