Miden Wallet ships as a Chrome/Firefox extension, iOS/Android app (Capacitor), and macOS app (Tauri). React + Zustand frontend; service-worker backend (Effector store + vault). The backend is the source of truth; the frontend syncs via intercom port messaging (src/lib/intercom/).
The main TypeScript/React application lives in src/. Put reusable UI in src/components/, screens and flows in src/screens/, app routing/providers in src/app/, and platform or domain logic in src/lib/:
lib/store/— Zustand (frontend)lib/miden/{back,front,sdk,psm}— wallet corelib/miden/transaction/— tx pipeline: initiate/complete/get/cancel/helper; index =generateTransaction+ loop (re-exported viamiden/activity)lib/platform/—isMobile/isIOS/isAndroid/isExtensionlib/mobile/— haptics, back-handlerlib/woozie/— hash-based router (navigate,goBack,useLocation,<Link>)lib/shared/types.ts— message types
Entry points such as popup.tsx, mobile-app.tsx, and desktop-app.tsx assemble each target. Browser tests are in playwright/tests/; blockchain, stress, iOS, and Android suites are under playwright/e2e/. Native shells live in ios/, android/, and src-tauri/; Capacitor plugins are in packages/. Static images and fonts belong in src/assets/, public/, screenshots/, and fonts/ as appropriate.
Use Node 22+ and Yarn v1. Copy .env.example to .env, then run yarn install.
yarn devrebuilds the Chrome extension in watch mode; loaddist/chrome_unpacked/in Chrome.yarn build:devnet— network-specific extension build.yarn build:chrome,yarn build:mobile, andyarn desktop:buildproduce platform builds.yarn mobile:ios:run[:devnet](iPhone 17 simulator default),yarn mobile:android,yarn tauri dev.yarn testruns Jest;yarn test:coverageenforces coverage thresholds.yarn test:e2eruns the basic Playwright extension suite serially;yarn test:e2e:blockchain:{testnet,devnet,localhost}andyarn test:e2e:mobile:{devnet,testnet}run live-network suites.yarn tstype-checks;yarn lintruns ESLint;yarn formatapplies Prettier. Run lint/format only before committing or when asked — not on every build.yarn storybookstarts component development on port 6006.
The extension manifest version comes from package.json, NOT public/manifest.json (webpack overrides it at webpack.public.config.js:69-70). Update both to keep them in sync, then rm -rf node_modules/.cache/webpack dist/ if the old version sticks.
- WASM client concurrency: the Miden WASM client is single-threaded; concurrent calls throw
recursive use of an object ... unsafe aliasing. Always wrap calls inwithWasmClientLockfromlib/miden/sdk/miden-client. - Duplicate dexie / duplicate
@miden-sdk/miden-sdk: the root web-sdk and the nested copy under@openzeppelin/miden-multisig-clienteach inline their own dexie into their wasm-glue chunk; two inlined dexies trip dexie's global guard at runtime (SW fails to register; mobile/desktop crash). The fix is already in place — every app vite config setsresolve.dedupe: ['dexie', '@miden-sdk/miden-sdk']andpackage.jsonpins/resolvesdexieto the web-sdk's inlined version. Keep it. To hunt a stray version, parse a built chunk's.js.mapforDEXIE_VERSION(plaingrep -rover node_modules misses it), and wipedist/chrome_unpackedbefore re-verifying —build:ext/build:bgdon't rimrafdist/. - Tailwind auto-flipping tokens:
text-black,bg-white,bg-gray-25/50/100,text-heading-graymap to CSS vars that flip with theme — do NOT adddark:variants on them. Adddark:only on fixed-palette colors (grey.*,pure-white,pure-black) or SVGfill={...}props. - i18n required: all user-facing text via
t('key')or<T id="key" />; CI blocks raw strings (yarn lint:i18n). New keys go inpublic/_locales/en/en.json(flat); placeholders use$name$. - Platform isolation: wrap platform-specific fixes with
isIOS()/isAndroid()/isMobile()fromlib/platform— never apply iOS fixes globally. - Haptics: tappable components get
hapticLight()(taps),hapticMedium()(toggles),hapticSelection()(tabs) fromlib/mobile/haptics. - Mobile file downloads:
<a download>does nothing in a WebView — useFilesystem.writeFile+Share.sharefrom@capacitor/{filesystem,share}whenisMobile(). - Balance loading:
fetchBalancesreads IndexedDB viagetAccount()(instant);AutoSynccallssyncState()separately. Never callsyncState()from the UI path. - Transaction states (
ITransactionStatus): Queued(0) → GeneratingTransaction(1) → Completed(2) / Failed(3). - Optimistic updates: snapshot previous state, apply, roll back on catch.
- Background auto-ops: use
startBackgroundTransactionProcessing(polls 5s × 5min, no modal), notopenLoadingFullPage. - Sanitized frontend state: the frontend receives state via
toFront(); vault/keys stay backend-only.
Read skills/miden-wallet-frontend/SKILL.md before implementing or reviewing wallet UI, CSS, motion, layout, or interaction changes. Reuse existing wallet components and semantic theme tokens before adding primitives or literal styles. Keep component-specific animation out of src/main.css; route nontrivial motion through Framer Motion and the reduced-motion-aware spring helpers. Interactive UI must use accessible semantics, appropriate haptics, localization, and platform isolation, then be verified on every affected surface.
- Message type in
src/lib/shared/types.ts - Handler in
src/lib/miden/back/actions.ts, registered inback/main.ts - Store action in
src/lib/store/index.ts - Expose via
useMidenContext()insrc/lib/miden/front/client.ts - When adding a new intercom message type, also update
src/lib/intercom/mobile-adapter.ts.
Two systems:
- Woozie (
src/lib/woozie/) — hash-based global router. - Navigator (
src/components/Navigator.tsx) — internal step flows (SendManager,SwapManager,EncryptedFileManager) viauseNavigator().
Onboarding (Welcome.tsx) and ForgotPassword.tsx use hash-based state (/#step-name), not Navigator. The in-progress transaction view is a routed page at /generating-transaction/:txId (desktop keepOpen: /generating-transaction-full/:txId) that observes its single tx row by id via a Dexie liveQuery; onClose guards on hash.includes('generating-transaction'), so keep that substring in any route rename. Send review is a routed page (/send/review?...); token/contact pickers are bottom-sheet drawers closed first by the flow's mobile back handler.
Back handlers (src/app/env.ts): registerBackHandler is stack-based. Mobile hardware/swipe back needs explicit handlers for global nav (MobileBackBridge), Navigator flows, state-based flows, and modals. When adding screens/routes, keep back handling correct.
- iOS
console.logis invisible to CLI tooling — usexcrun simctl spawn booted log stream --predicate 'process == "App"', and verify UI fixes withxcrun simctl io booted screenshot. - Grey bar at the bottom on iOS →
100dvhmisses safe areas; use100%+env(safe-area-inset-*)padding on themobile.htmlbody. - New Swift files must be registered in four
project.pbxprojsections (PBXBuildFile,PBXFileReference, AppPBXGroup,PBXSourcesBuildPhase) — the App target does not auto-discover them. - Custom Capacitor plugins (iOS) use manual registration: also call
bridge?.registerPluginInstance(MyPlugin())incapacitorDidLoad()(AppViewController.swift), or JS calls return{"code":"UNIMPLEMENTED"}. - New Capacitor plugins:
yarn add @capacitor/<name> && yarn mobile:sync, plus a ProGuard-keeprule inandroid/app/proguard-rules.pro. - Mobile bottom nav is a native overlay (iOS
UIWindow, AndroidNavbarOverlayManager), wired insrc/app/providers/DappBrowserProvider.tsx. - Desktop (Tauri): clear state with
rm -rf ~/Library/WebKit/{com.miden.wallet,miden-wallet}; dApp requests round-trip via base64-encodedhttps://miden-wallet-request/{payload}URL interception.
Co-locate Jest/React Testing Library tests as *.test.ts or *.test.tsx. Name Playwright scenarios *.spec.ts. Add regression tests for behavior changes and mock platform boundaries rather than live services in unit tests — mock lib/intercom for frontend tests and wrap with WalletStoreProvider + MidenContextProvider. Global Jest coverage must remain at least 95% for branches, functions, lines, and statements.
Gotchas:
jest.mock()paths must match the import path used in source (e.g.,'lib/miden/back/vault', not'./vault').window.location.reloadcan't be mocked in jsdom — wrap calls in try/catch.afterEach(() => testRoot.unmount())to prevent React cross-test pollution.
E2E: MIDEN_E2E_TEST=true exposes window.__TEST_STORE__ and window.__TEST_INTERCOM__ (zero production impact). The blockchain harness runs against a live network — use the :<network> scripts so harness endpoints and the bundled MIDEN_NETWORK stay matched.
TypeScript is strict. No any, no as — use explicit domain types, and preserve the configured absolute imports (app/..., lib/..., shared/...). Prettier: 120-column width, two-space indentation, single quotes, semicolons, trailing commas. ESLint enforces formatting and ordered imports. Name React components and files in PascalCase, hooks as useSomething, and utilities in camelCase or established kebab-case modules. yarn format to fix.
- Commit messages: single-line, short, imperative. Never sign commits (no
Co-Authored-By). - Never
git pushwithout explicit request. - Stay within requested scope — don't modify files beyond the task.
- Update
CHANGELOG.mdwith one entry per PR/task (not per fix). Never add an entry under a version that's already been published — checkgh api repos/0xMiden/wallet/releases/latestand use a strictly-higher(TBD)section (add one if missing); don't trust the file header alone. - PRs should explain the user impact, testing performed, and relevant issue; include screenshots or recordings for UI changes. Call out platform-specific effects and configuration changes. Never commit secrets from
.envor machine-local dependency paths. - If the wallet PR depends on an unpublished web-sdk change, put the verbatim marker
Web SDK PR: #N(orWeb SDK PR: 0xMiden/web-sdk#N) on its own line in the PR description — prose mentions do NOT trigger the linked-PR CI pipeline. Local parity:scripts/dev-with-web-sdk-pr.sh [N|--clear].