Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
86db36b
feat: use multisig sdk instead of the base http client
0xnullifier Mar 5, 2026
b8a46a2
fix: address PSM integration review findings (#155)
WiktorStarczewski Mar 11, 2026
08ab29e
feat: use the new `createTransactionProposalRequest` method for creat…
0xnullifier Mar 12, 2026
0009fb7
chore: use deployed psm url
0xnullifier Mar 13, 2026
08346ab
feat: add a option to choose recovery type during onboarding
0xnullifier Mar 16, 2026
71d8212
chore: merge conflicts
0xnullifier Mar 19, 2026
2b00a3e
feat: make onboarding screen for gaurdian aligned with the rest of th…
0xnullifier Mar 19, 2026
c015b44
chore: changelog
0xnullifier Mar 19, 2026
405bfd0
chore: update translation files
github-actions[bot] Mar 19, 2026
ed288d9
feat: add psm account provider to properly handle service worker tran…
0xnullifier Mar 19, 2026
bca5958
feat: add account recovery flow for gaurdian backed accounts
0xnullifier Mar 20, 2026
d7a5a4f
feat: wip
0xnullifier Mar 23, 2026
39eba6b
chore: bump up versions
0xnullifier Mar 24, 2026
2c31050
fix: move psm sync to frontend
0xnullifier Mar 25, 2026
8950d20
chore: update translation files
github-actions[bot] Mar 25, 2026
208aef6
Merge branch 'main' into utk-psm-integration
0xnullifier Apr 16, 2026
79074fe
feat: bump up deps, make guardian work on mobile, make sync work for …
0xnullifier Apr 19, 2026
934f575
Merge remote-tracking branch 'origin/main' into utk-psm-integration
0xnullifier Apr 20, 2026
2a7b723
chore: drop verbose debug logs in intercom and miden-client
0xnullifier Apr 20, 2026
cf19fe3
feat: guardian switch transaction for PSM accounts
0xnullifier Apr 20, 2026
0f30153
feat: per-stage label for PSM/guardian transactions
0xnullifier Apr 21, 2026
e1c076d
Merge remote-tracking branch 'origin/main' into utk-psm-integration
0xnullifier Apr 21, 2026
93216a6
feat: account lists badge
0xnullifier Apr 21, 2026
4ef5cca
feat: import-recovery-method screen with custom guardian URL for PSM …
0xnullifier Apr 23, 2026
b5a5617
fix: readable text in dark mode on onboarding import screens
0xnullifier Apr 23, 2026
97d796f
Merge remote-tracking branch 'origin/main' into utk-psm-integration
0xnullifier Apr 23, 2026
c68fe81
refactor: rename psm to guardian
0xnullifier Apr 23, 2026
13b8d83
chore: update translation files
github-actions[bot] Apr 23, 2026
68e8c2c
test: mock openzeppelin packages and update test signatures after merge
0xnullifier Apr 23, 2026
38c4557
chore: fix lint warnings for CI --max-warnings 0
0xnullifier Apr 23, 2026
5114335
chore: reorder imports to match CI lint rules (patched eslint-plugin-…
0xnullifier Apr 23, 2026
780df88
test: add guardian tests to meet 95% coverage thresholds
0xnullifier Apr 23, 2026
f08b13e
fix: pre-build content scripts in dev + onboarding recovery-method st…
0xnullifier Apr 23, 2026
34f010f
Merge remote-tracking branch 'origin/main' into utk-psm-integration
0xnullifier Apr 23, 2026
5dc8d3a
fix(sw): use @miden-sdk/miden-sdk/lazy to avoid TLA crash in backgrou…
0xnullifier Apr 23, 2026
b953e5c
Merge remote-tracking branch 'origin/main' into utk-psm-integration
0xnullifier May 5, 2026
143ff8b
test: extend coverage for constants, vault, transactions
0xnullifier May 6, 2026
cc9f4c3
feat: per-network guardian endpoint map (testnet/devnet)
0xnullifier May 6, 2026
bcecdf3
test: drop obsolete useConnectivityIssues hook tests
0xnullifier May 6, 2026
0150689
Merge branch 'main' into utk-psm-integration
0xnullifier May 6, 2026
a0e7b1c
ci: lint
0xnullifier May 6, 2026
23d8331
ci: tsc
0xnullifier May 6, 2026
f052d41
feat(guardian): switch default auth scheme to ECDSA
0xnullifier May 1, 2026
ec0a4cd
feat(guardian): 3-key (hot+cold+guardian) account creation
0xnullifier May 1, 2026
18cf412
feat(guardian): split signWord into cold/hot paths keyed by hotPublicKey
0xnullifier May 4, 2026
7ad8dbc
feat(ios): SE-backed HotKey plugin with recoverable secp256k1 + on-de…
0xnullifier May 4, 2026
ab9fee5
fix(mobile): drop inlineDynamicImports to avoid TLA wrapper crash in …
0xnullifier May 4, 2026
973bd74
feat(android): StrongBox-backed HotKey plugin with recoverable secp256k1
0xnullifier May 4, 2026
375be34
feat(guardian): replace-hot-key flow + cold co-signs switch_guardian
0xnullifier May 5, 2026
49ca879
fix(guardian): refresh sync state and cache after hot-key rotation
0xnullifier May 5, 2026
d9048a7
chore(deps): bump @openzeppelin/{guardian,miden-multisig}-client to 0…
0xnullifier May 11, 2026
cee363e
feat(guardian): switch-guardian cold-signing confirmation + Phase 7 c…
0xnullifier May 11, 2026
731dc33
feat(guardian): defer post-recovery hot-key activation to a banner
0xnullifier May 12, 2026
42d6094
refactor(guardian): collapse multisigService/signingService into one …
0xnullifier May 12, 2026
c650267
fix(guardian): fire accountsUpdated on SW-side swapHotKey so popups s…
0xnullifier May 12, 2026
f4b2f2f
feat(guardian): reveal hot key + reshape reveal private key for 3-key…
0xnullifier May 12, 2026
2129763
fix(guardian): pass 'ecdsa' scheme to nextGuardian.getPubkey in final…
0xnullifier May 12, 2026
44a369d
feat: bump up package
0xnullifier May 14, 2026
75fd008
Merge remote-tracking branch 'origin/v0' into utk-psm-integration
0xnullifier Jun 17, 2026
57cd91e
chore: bump guardian packages to the latest
0xnullifier Jun 17, 2026
bd392b6
fix: guardian sync memory leak from leaked per-init web-client workers
0xnullifier Jun 6, 2026
6a00564
Merge origin/utk-psm-integration into utk-3keys
0xnullifier Jun 17, 2026
8e512d4
fix: pin secp256k1 to 0.21.1 to prevent xcode errors
0xnullifier Jun 22, 2026
0822463
Merge remote-tracking branch 'origin/main' into utk-3keys
0xnullifier Jun 23, 2026
a7e84fc
chore: update translation files
github-actions[bot] Jun 23, 2026
59ea180
test: for secure hot key facade and fix broken ones
0xnullifier Jun 23, 2026
430c6fc
ci: coverage and lint
0xnullifier Jun 23, 2026
72e48ef
ci: tsc
0xnullifier Jun 23, 2026
9ad6157
chore: remove scripts and unneccesary changes
0xnullifier Jun 24, 2026
a7245c7
fix: restore bare miden-sdk mt/lazy alias in extension config to fix …
0xnullifier Jun 24, 2026
605b750
fix(guardian): cold-sign background auto-consume to avoid biometric p…
WiktorStarczewski Jun 25, 2026
796278c
feat(guardian): migrate legacy single-key Guardian accounts to 3-key …
WiktorStarczewski Jun 25, 2026
a5dae27
feat(guardian): set update_guardian procedure threshold after activat…
WiktorStarczewski Jun 25, 2026
291cd49
Merge remote-tracking branch 'origin/main' into wiktor/227-cold-autoc…
WiktorStarczewski Jun 25, 2026
d0052ad
test(guardian): on-chain auth reader + assert 3-key auth structure in…
WiktorStarczewski Jun 25, 2026
10a6bc1
test(guardian): extend on-chain auth reader to mobile (iOS + Android)…
WiktorStarczewski Jun 25, 2026
a22dfc3
fix(guardian): address review — legacy activation, hot-key cache, log…
WiktorStarczewski Jun 26, 2026
03d86ee
test(guardian): cover proposal builders + procedure-threshold guard t…
WiktorStarczewski Jun 26, 2026
5229214
fix(guardian): second-review fixes — signer-by-key routing, rotation …
WiktorStarczewski Jun 26, 2026
8ecfb48
fix(guardian): per-account endpoint + reconcile structural ops on app…
WiktorStarczewski Jun 26, 2026
f45b679
fix(guardian): only sync Guardian accounts that have a hot key
WiktorStarczewski Jun 26, 2026
49bd9df
fix(guardian): re-register rotated state on guardian after structural…
WiktorStarczewski Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### 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)

## 1.15.2 (2026-06-22)
Expand Down Expand Up @@ -33,6 +36,8 @@

### 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)

Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ xcrun simctl spawn booted notifyutil -p com.apple.BiometricKit_Sim.fingerTouch.m
- Grey bar at bottom → `100dvh` doesn't account for safe areas. Use `100%` + `env(safe-area-inset-*)` padding on `mobile.html` body.
- Debug UI text should be `select-text` so errors are copyable.

### Adding Swift files to the App target
The App target in `ios/App/App.xcodeproj/project.pbxproj` does NOT auto-discover Swift files in the `App/` source directory. New files must be registered in four sections: `PBXBuildFile`, `PBXFileReference`, the App `PBXGroup` children, and the `PBXSourcesBuildPhase` files list. Pattern: see `LocalBiometricPlugin.swift` or `HotKeyPlugin.swift` entries.

### Adding a custom Capacitor plugin (iOS)
Capacitor on this app uses **manual** registration — not the `CAPBridgedPlugin` auto-discovery you'd get on a stock Capacitor app. After creating `MyPlugin.swift` and wiring it into the four pbxproj sections above, you also have to call `bridge?.registerPluginInstance(MyPlugin())` inside `capacitorDidLoad()` in `ios/App/App/AppViewController.swift`. Skip this step and JS calls land as `{"code":"UNIMPLEMENTED"}` even though the class compiled fine.

### Native navbar overlay
Mobile hides React footer and renders bottom nav as native pill (iOS: `MidenNavbarOverlayWindow` `UIWindow`; Android: two-instance `NavbarOverlayManager` with Activity-scoped + Dialog-scoped `NavbarView`). Plugin methods: `showNativeNavbar`, `setNavbarSecondaryRow`, `setNavbarAction`, `morphNavbar{Out,In}`. Events: `nativeNavbarTap`, `nativeNavbarSecondaryTap`, `nativeNavbarActionTap`. Wiring: `src/app/providers/DappBrowserProvider.tsx`. Android gotchas: don't use `MATCH_PARENT` children in `WRAP_CONTENT` parents (1878px buttons); `Dialog.setLayout` must follow `setContentView`; shadow must be on the view owning the background drawable.

Expand Down
10 changes: 10 additions & 0 deletions __mocks__/@openzeppelin/miden-multisig-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ export const buildMultisigStorageSlots = jest.fn();
export const buildGuardianStorageSlots = jest.fn();
export const storageLayoutBuilder = jest.fn();

// Update-signers + summary builders used by createReplaceHotKeyProposal. The
// real implementations touch WASM; tests mock-or-spy as needed.
export const buildUpdateSignersTransactionRequest = jest.fn(async () => ({
request: { kind: 'update-signers-request' },
salt: { toHex: () => 'salt-hex' }
}));
export const executeForSummary = jest.fn(async () => ({
serialize: () => new Uint8Array([0xab])
}));

export class StorageLayoutBuilder {
constructor(..._args: unknown[]) {}
}
4 changes: 4 additions & 0 deletions __mocks__/lib/miden/back/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export class Vault {
return state.accounts;
}

// Best-effort on-unlock migration of legacy single-key Guardian accounts to
// the 3-key model — a no-op in the mock (no real accounts/keys to migrate).
async migrateLegacyGuardianAccounts() {}

async fetchSettings() {
return state.settings;
}
Expand Down
1 change: 1 addition & 0 deletions android/.idea/deploymentTargetSelector.xml

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

4 changes: 4 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Biometric authentication for hardware security
implementation 'androidx.biometric:biometric:1.1.0'
// secp256k1 ECDSA + Keccak-256 for the Guardian hot-key signing path
// (HotKeyPlugin). Keystore wraps the 32-byte k256 secret with RSA-OAEP;
// BouncyCastle handles everything outside the Keystore boundary.
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
}

apply from: 'capacitor.build.gradle'
Expand Down
6 changes: 6 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@
-keep class com.niceforyou.capacitor.barcodescanner.** { *; }
-keep class com.google.mlkit.vision.barcode.** { *; }

# BouncyCastle (secp256k1 + Keccak-256) — used by HotKeyPlugin for the
# Guardian hot-key signing path. Keep the low-level crypto + asn1 classes
# we touch reflectively via JCE provider lookup paths.
-keep class org.bouncycastle.** { *; }
-keep class com.miden.wallet.HotKeyPlugin { *; }

# Don't warn about missing classes in optional dependencies
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
Expand Down
Loading
Loading