feat: SDK hardening integration + MidenClient singleton unification#189
Open
WiktorStarczewski wants to merge 26 commits intomainfrom
Open
feat: SDK hardening integration + MidenClient singleton unification#189WiktorStarczewski wants to merge 26 commits intomainfrom
WiktorStarczewski wants to merge 26 commits intomainfrom
Conversation
…lock Depends on miden-client wiktor-test-followups branch via file: ref until the SDK change is merged and a new version is published.
Transport failures on private-note sends now mark the tx Completed (the on-chain commit is durable) with a transportPending flag + attempt counter. A background retry loop in the TransactionProcessor finds these, backs off exponentially, and calls the SDK's resendPrivateById until the recipient receives the note blob. Without this, a transport-side failure left the sender's asset effectively lost — the recipient has no way to discover private notes from on-chain data alone. The retry runs on SW startup + every TransactionProcessor tick; after 20 attempts the tx is flagged Failed so the user sees a clear terminal state. Depends on the SDK helper landed on miden-client wiktor-test-followups.
Adds STRESS_TRANSPORT_FAIL_PROB env var. When set (e.g. 0.1), each private-note send has that probability of having its SendNote gRPC call intercepted and aborted via Playwright page.route — forcing the wallet into its transport-pending retry path. The retry loop inside generateTransactionsLoop should then deliver the note on the next tick via the SDK's resendPrivateNoteById. Final balance conservation should still hold; failure to deliver = note loss = test fail. Default is 0 so regular stress runs match historical behavior; passing STRESS_TRANSPORT_FAIL_PROB=0.1 validates the transport retry end-to-end. Skipped for concurrent ops to keep the signal clean.
…re bridge Collapse MidenClientSingleton from two instances (instance + instanceWithOptions) to one. Keystore callbacks are wired permanently at MidenClient.create time via a late-binding bridge module; Effector unlocked/locked events re-point the insert-key slot to the active vault. Per-tx sign callbacks go through the bridge's activeSignCallback slot with a concurrent-set guard. Key changes: - New keystore-bridge.ts: pure callback-slot module (insertKey + sign + getKey) - New keystore-wiring.ts: Effector event subscriptions, called from all 3 entry points (SW main.ts, mobile-adapter, desktop-adapter) - Vault gains encryptKeystoreEntry() method; KEK never leaves the Vault class - getMidenClient() no longer accepts options; tests use MidenClientInterface.create directly - withProverFallback builds a fresh TransactionProver.newRemoteProver per-call instead of relying on client.defaultProver (which silently falls back to local after a single failure and never recovers) - Wallet mutex stays active as belt-and-suspenders Validated via stress suite: 5-op run with explicit remote prover shows all ops completing in ~1.3-2.2s (vs 3.2-8.5s baseline with dispose-recreate per-tx). Balance conservation held across all test configurations.
The SDK's proveTransactionWithProver now takes &TransactionProver (by reference), so the JS handle is preserved across calls. Remove the wallet-side newRemoteProver-per-call workaround and rely on MidenClient's defaultProver (set from proverUrl at create time).
…al file: reference" This reverts commit 20308af.
…pass-through) The SDK's internal _serializeWasmCall chain + single-instance MidenClient design handles WASM call serialization. The wallet's AsyncMutex was belt-and-suspenders from before the singleton unification — needed when two client instances had independent SDK chains. Validated by 2 consecutive 5-op stress runs (seeds 301, 302) with mixed private/public sends, 0 failures, conservation held on both. A third run failed during faucet deployment (devnet RPC flake, unrelated to wallet code). Also restores @miden-sdk/miden-sdk to file:../miden-client/crates/web-client.
Collaborator
Does it ? I mean surely not doing a simple console.time around like this export async function getMidenClient(options?: MidenClientCreateOptions): Promise<MidenClientInterface> {
if (options) {
console.time('Creating MidenClient with options');
const client = await midenClientSingleton.getInstanceWithOptions(options);
console.timeEnd('Creating MidenClient with options');
return client;
}
console.time('Getting MidenClient instance');
const client = await midenClientSingleton.getInstance();
console.timeEnd('Getting MidenClient instance');
return client;
}it consistently takes less than 50ms so idk if this unification does anything in that regards and if it does take 1-2s just to create the client and destroy instance then there is bigger fish to fry |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integrates the SDK hardening improvements into the wallet and unifies the
MidenClientSingletonfrom two instances (dispose-and-recreate per-tx) to one long-lived singleton with a late-binding keystore bridge. Net result: cleaner architecture, 2.5× faster sends, and robust error handling for partial-failure scenarios.SDK hardening integration
Wallet-side integration for 5 SDK improvements (each has its own SDK PR):
Typed sign-callback failure recovery — when the wallet gets locked mid-transaction, the sign callback throws with
{ reason: 'locked' }. The TransactionProcessor readsmidenClient.lastAuthError()and leaves the tx Queued for retry after unlock, instead of marking it Failed (which previously caused note loss).ApplyTransactionAfterSubmitFailed handling — when a tx submits on-chain but the local apply step fails, the wallet marks it Completed (not Failed) using the SDK's new
errorCodedispatch. The user sees "Transaction sent" instead of a confusing "Transaction failed" for a tx that IS on chain.Private-note transport retry — if the P2P blob delivery fails after a private send commits on-chain, the wallet marks the note
transportPendingand retries viaresendPrivateNoteById()with exponential backoff in the background.WASM call serialization + waitForIdle — the wallet's
lock()action useswaitForIdle()to drain in-flight WASM operations before clearing the vault key, preventing the sign-callback race that caused 7/7 executeTransaction errors in stress testing.MidenClient singleton unification
Collapses
MidenClientSingletonfrom two instances (instance+instanceWithOptions) to one long-lived singleton:keystore-bridge.ts— pure callback-slot module (activeInsertKey+activeSignCallback). The SDK's permanent keystore callbacks delegate to these mutable slots.keystore-wiring.ts— subscribes to Effectorunlocked/lockedevents and re-points the bridge's insert-key slot to the active vault'sencryptKeystoreEntrymethod. Called from all 3 entry points (SWmain.ts,mobile-adapter.ts,desktop-adapter.ts).Vault.encryptKeystoreEntry()— new method that encrypts a keypair under the vault's KEK without exposing the key outside theVaultclass.getMidenClient()no longer accepts options — tests that need a specific seed callMidenClientInterface.create({seed})directly.withProverFallbackusesdefaultProverpath — relies on the SDK'sproveTransactionWithProver(&prover)by-reference fix (#2062) to keep the JS prover handle alive across calls.Performance
The singleton eliminates the per-tx client dispose-and-recreate cycle (~1-3s overhead per send). Combined with the by-reference prover fix:
Stress test results
SDK dependencies
This PR depends on the following SDK changes (all on
0xMiden/miden-client):lastAuthError()— typed sign failure recoveryApplyTransactionAfterSubmitFailederror varianterrorCodedispatch (depends on #2059)resendPrivateNoteById()transport retryproveTransactionWithProver(&prover)by-reference fixwaitForIdle()+ WASM serialization (exploratory)The wallet's
@miden-sdk/miden-sdkdependency usesfile:../miden-client/crates/web-clientduring development. Once the SDK PRs land and a release is cut, the wallet will pin to the published version.Files changed
New files (3):
src/lib/miden/sdk/keystore-bridge.ts— callback-slot bridge modulesrc/lib/miden/sdk/keystore-bridge.test.ts— 12 unit testssrc/lib/miden/back/keystore-wiring.ts— Effector event wiringModified files (14):
src/lib/miden/sdk/miden-client.ts— singleton simplificationsrc/lib/miden/sdk/miden-client-interface.ts— drop keystore options, wire bridge, defaultProver pathsrc/lib/miden/sdk/miden-client.test.ts— remove dispose-recreate testsrc/lib/miden/sdk/miden-client-interface.test.ts— update for bridge wiringsrc/lib/miden/back/vault.ts—encryptKeystoreEntry,persistKeystoreEntryhelper, export storage keyssrc/lib/miden/back/vault.test.ts— round-trip test for encryptKeystoreEntrysrc/lib/miden/back/actions.ts— lock comment updatesrc/lib/miden/back/dapp.ts— comment clarifying signData is not in bridge pathsrc/lib/miden/back/main.ts— wireKeystoreBridge before intercomsrc/lib/miden/back/main.test.ts— mock keystore-wiringsrc/lib/miden/activity/transactions.ts— bridge sign callback with try/finallysrc/lib/intercom/mobile-adapter.ts— wireKeystoreBridge in initsrc/lib/intercom/mobile-adapter.test.ts— mock keystore-wiringsrc/lib/intercom/desktop-adapter.ts— wireKeystoreBridge in initTest plan
yarn lint && yarn format— cleanyarn test— 1661/1661 pass