Skip to content

feat(#142): restore sessions & trades from mnemonic handshake - #204

Closed
codaMW wants to merge 7 commits into
MostroP2P:mainfrom
codaMW:feat/142-restore-from-mnemonic
Closed

feat(#142): restore sessions & trades from mnemonic handshake#204
codaMW wants to merge 7 commits into
MostroP2P:mainfrom
codaMW:feat/142-restore-from-mnemonic

Conversation

@codaMW

@codaMW codaMW commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What this does

Implements the client side of restore sessions & trades from mnemonic (#142): after a reinstall, entering the 12-word phrase restores not just the identity but the user's active trades.

Before this work, importing a mnemonic restored only the identity keys the recover flag was silently ignored (_recover), so no RestoreSession was ever sent and trades were lost. This PR wires the full RestoreSession handshake, proves it end-to-end against a regtest daemon, and lays the reconstruction foundation.

Status: not merge-ready. The handshake and trade recovery are complete and E2E-proven. The final hop surfacing recovered orders in My Trades needs one architecture decision (see Open design question below). Marking as draft.

How it works

Restore correlates differently from every other action, and this was the core of the work:

  • The app sends RestoreSession from a freshly-derived trade key. The gift-wrap Seal is signed with the identity key, so on the daemon side event.identity = master key and event.sender = trade key.
  • The daemon looks up the user's trades by master key (restore_session.rs: master_key = event.identity) and replies with Payload::RestoreData addressed to the trade key (event.sender).
  • RestoreSession carries no request_id, so the reply is correlated purely by pubkey a dedicated take_matching_restore(pubkey) that skips the nonce gate the order path uses.
  • The dispatcher gains an Action::RestoreSession arm that decodes RestoreData, resolves the waiting caller, and returns the RestoreSessionInfo.

This mirrors create_order's send/await pattern wholesale, minus the order payload and request_id.

Changes

  • identity.rs import_from_mnemonic now honors recover (was a _recover stub) and calls restore_session().
  • mostro/actions.rs restore_session builder (Message::new_restore(None)), wraps with identity key in the Seal + derived trade key as sender.
  • api/orders.rs
    • PendingRequestKind::Restore + DaemonReply::Restored(RestoreSessionInfo)
    • take_matching_restore(pubkey) pubkey-only correlation (no request_id)
    • restore_session() derive trade key, send, subscribe, register pending, await reply
    • Action::RestoreSession dispatcher arm
    • #[flutter_rust_bridge::frb(ignore)] on restore_session() (internal returns a mostro-core type FRB can't bridge; called by import_from_mnemonic, never from Dart)
    • Reconstruction foundation: restore_session plants order_id -> trade_index mappings for recovered orders; ingest_order_event marks is_mine = true when an order_id is known via restore.

Testing E2E proven against a regtest daemon

Two integration tests (#[ignore], require a live regtest stack on ws://localhost:7000), run individually:

cargo test --lib restore_session_roundtrip     -- --ignored --nocapture
cargo test --lib trade_then_restore_recovers_order -- --ignored --nocapture
  1. restore_session_roundtrip bare handshake round-trips: app publishes RestoreSession (kind-14) -> daemon receives and replies RestoreData to the trade key -> dispatcher resolves the waiter. Returns Ok with an empty result for a fresh identity (0 trades).

  2. trade_then_restore_recovers_order real recovery: create a Sell order under identity A (daemon-confirmed), then restore as A. The daemon looks up by master key and returns the order created under a different trade index, proving master-key correlation on the wire:

    RestoreData(RestoreSessionInfo { restore_orders: [
        RestoredOrdersInfo { order_id: <uuid>, trade_index: 1, status: "pending" }
    ], restore_disputes: [] })
    

Existing suite: 107 passed; 0 failed (cargo test --lib). cargo build clean.

Shaking this out end-to-end also surfaced two regtest-env issues unrelated to the code my daemon was on transport = "gift-wrap" (v1, kind-1059) instead of nip44 (v2, kind-14), and the transport key was under [nostr] instead of [mostro] in settings.toml. Both fixed locally; noting in case it helps others' v2 setup.

Open design question (the one thing I need a call on)

Recovered orders don't yet appear in My Trades. RestoredOrdersInfo carries only {order_id, trade_index, status}, but list_trades() reads full TradeInfo records from the DB which need the full order data plus role/current_step/counterparty. The order-book is_mine flag (fed by Kind-38383) is a separate store from that DB.

So the final reconstruction step needs a direction:

  • (A) Client hydrates the full order from its Kind-38383 event and synthesizes a TradeInfo (deriving role/current_step from status), then writes it to the trades DB.
  • (B) RestoreData / RestoredOrdersInfo carries richer order data so the client can build a complete TradeInfo directly (cross-repo change to mostro-core + daemon).

I've committed the mapping + is_mine-by-order-id foundation; I'll build the TradeInfo synthesis once the approach is confirmed, to avoid guessing the intended data model.

Notes for review

  • The two E2E tests are #[ignore]d (won't run in CI) and one hardcodes my regtest daemon pubkey. Happy to make them env-configurable or drop them from the PR whichever you prefer.
  • No Dart/UI changes here; the restore UI wiring (the 12-word screens) comes after reconstruction lands.

Refs #142

Summary by CodeRabbit

  • New Features

    • Added session recovery when importing an identity with recovery enabled.
    • Added support for restoring previously active orders and their trade associations.
    • Added restore-session communication with the daemon, including response handling and timeout protection.
  • Bug Fixes

    • Recovery now clearly rejects identities using privacy mode, which is not supported for restoration.
    • Recovered orders are correctly recognized as belonging to the current identity.

codaMW added 6 commits July 21, 2026 02:53
… UNRESOLVED

Compiling: _recover fix, restore_session builder, PendingRequestKind::Restore,
DaemonReply::Restored, take_matching_restore, restore_session() send-half.

BLOCKED: key-choice (trade-key vs master-key) + await path. Send-half currently
subscribes on the wrong key until resolved. Refs MostroP2P#142
…a Dart API

restore_session returns mostro-core RestoreSessionInfo which FRB can't bridge;
it's called internally by import_from_mnemonic, not from Dart. Refs MostroP2P#142
…t daemon

Verified send→receive: app publishes RestoreSession (kind-14, identity in Seal,
trade key as sender), daemon looks up by master key and replies RestoreData to
the trade key, dispatcher resolves the waiter. 0-order case confirmed. Refs MostroP2P#142
Create identity A, create a Sell order under A (daemon-confirmed), then
restore_session() from A: daemon looks up by master key and returns the order
made under a different trade index — confirms master-key correlation on the
wire. restore_orders=[{order_id, trade_index:1, status:pending}]. Refs MostroP2P#142
…_mine)

restore_session plants order_id -> trade_index mappings for each recovered
order; ingest_order_event marks is_mine=true when an order_id is known via
restore. Compiles clean; existing suite green.

INCOMPLETE — recovered orders don't yet reach My Trades: list_trades() reads
TradeInfo records from the DB, but restore yields only {order_id, trade_index,
status} and the order-book is_mine flag is a separate store. Needs a design
decision on TradeInfo synthesis vs. richer RestoreData. Refs MostroP2P#142
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@codaMW, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 430c6324-61b8-438a-86ce-67e9b95658f1

📥 Commits

Reviewing files that changed from the base of the PR and between c68b3b0 and c92efe9.

📒 Files selected for processing (3)
  • rust/src/api/identity.rs
  • rust/src/api/orders.rs
  • rust/src/mostro/actions.rs

Walkthrough

Mnemonic recovery now invokes a restore-session API, which exchanges a restore request and reply with Mostro, stores recovered trade mappings, and identifies restored orders as owned. Privacy-mode identities reject recovery.

Changes

Restore Session Recovery

Layer / File(s) Summary
Restore request construction
rust/src/mostro/actions.rs
Adds a gift-wrapped restore_session action using an empty restore payload and the documented signing keys.
Restore reply correlation
rust/src/api/orders.rs
Adds restore reply and pending-request variants, correlates replies by pubkey, and dispatches RestoreData responses to waiting callers.
Recovery orchestration and order hydration
rust/src/api/orders.rs, rust/src/api/identity.rs
Adds the exported restore flow with timeout handling, stores recovered trade mappings, marks mapped orders as owned, and triggers recovery from mnemonic import while rejecting privacy-mode identities. Includes ignored end-to-end coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Suggested reviewers: grunch

Poem

I’m a rabbit with keys in a bundle,
Watching restored trade pathways rumble.
Mnemonics call, Mostro replies,
Mapped orders hop into our eyes.
Privacy gates keep secrets tight—
Recovery springs to life tonight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding mnemonic-based session and trade restore handshake support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rust/src/api/orders.rs (1)

1773-1802: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

No log for a late (post-timeout) RestoreData reply.

Every other correlated arm (NewOrder, take, add-invoice) logs the case where pending.tx is None (detached after timeout) for observability. Here, if take_matching_restore returns a record whose tx is already None, the if let Some(tx) = pending.tx silently does nothing — no log at all, unlike the sibling arms.

🩹 Suggested fix
                     if let Some(pending) = take_matching_restore(trade_pubkey_hex) {
                         if let Some(tx) = pending.tx {
                             let _ = tx.send(DaemonReply::Restored(info.clone()));
                             crate::api::logging::blog_info("gift-wrap", format!(
                                 "RestoreData: notified waiting restore_session ({} orders, {} disputes)",
                                 info.restore_orders.len(),
                                 info.restore_disputes.len()
                             ));
+                        } else {
+                            crate::api::logging::blog_info("gift-wrap", format!(
+                                "RestoreData: late reply for timed-out restore on trade={}",
+                                &trade_pubkey_hex[..8]
+                            ));
                         }
                     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/api/orders.rs` around lines 1773 - 1802, Update the RestoreSession
handling around take_matching_restore so a matching pending record with
pending.tx set to None emits an informational log for the late post-timeout
RestoreData reply. Preserve the existing notification path when a sender is
present and the existing no-waiting-caller log when no pending record is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/src/api/identity.rs`:
- Around line 167-178: Update import_from_mnemonic to read the authoritative
privacy mode via reputation::get_privacy_mode() before calling
load_identity_from_mnemonic, pass that value instead of false, and use the same
value for the recover guard so PrivacyModeRecoveryUnavailable is enforced
consistently.

In `@rust/src/mostro/actions.rs`:
- Around line 462-479: Correct the `restore_session` doc comment to state that
the request is sent from the identity key while the daemon routes its reply to
the trade key derived from the rumor sender. Keep the payload and NIP-59 Gift
Wrap descriptions unchanged, and align the outer documentation with the inline
comments and existing correlation logic.

---

Nitpick comments:
In `@rust/src/api/orders.rs`:
- Around line 1773-1802: Update the RestoreSession handling around
take_matching_restore so a matching pending record with pending.tx set to None
emits an informational log for the late post-timeout RestoreData reply. Preserve
the existing notification path when a sender is present and the existing
no-waiting-caller log when no pending record is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0a999d0-821a-44bf-a16d-9ec44897711d

📥 Commits

Reviewing files that changed from the base of the PR and between 436847b and c68b3b0.

📒 Files selected for processing (3)
  • rust/src/api/identity.rs
  • rust/src/api/orders.rs
  • rust/src/mostro/actions.rs

Comment thread rust/src/api/identity.rs
Comment thread rust/src/mostro/actions.rs
- import_from_mnemonic: source privacy_mode from reputation::get_privacy_mode()
  instead of hardcoded false. The guard previously read info.privacy_mode (always
  false), so PrivacyModeRecoveryUnavailable could never fire while
  restore_session() read the real flag — a Full-Privacy user could have triggered
  restore. Now sourced once and used for both load and guard.
- actions::restore_session: fix doc comment — the daemon addresses its reply to
  the trade key (event.sender), not the identity key. Doc now matches the inline
  comment and take_matching_restore correlation logic.
- dispatcher RestoreData arm: log the post-timeout late reply (pending.tx = None)
  for parity with the NewOrder/take/add-invoice arms.
Refs MostroP2P#142

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict review — changes requested

The RestoreSession handshake is a useful foundation, but the recovery flow described by #142 and the repository contract is not end-to-end functional yet.

Blocking findings

  1. The recovery path is unreachable from the app. The only production mnemonic import path still calls importFromMnemonic(..., recover: false) (lib/core/services/identity_service.dart:74-76), and the Account import UI routes through that method. No production caller passes recover: true, so reinstall/import never sends this RestoreSession request.

  2. The restore result does not reconstruct a usable trade/session. restore_session() only writes order_id -> trade_index mappings (rust/src/api/orders.rs:3400-3411). It does not create TradeInfo rows or Session objects, restore disputes/chat, or fetch enough order data to do so. list_trades() reads only the trades DB, therefore recovered trades remain absent from My Trades and session-dependent actions/chat still fail. This leaves steps 4-6 of specs/004-mostro-p2p-client/contracts/identity.md:36-42 unimplemented.

  3. The derivation counter is not resynchronized. Import starts with index 0 and the restore request consumes index 1, but processing RestoreData never raises IdentityInfo.trade_key_index to the maximum recovered index. The next order can therefore derive an already-used key and be rejected with InvalidTradeIndex. This also prevents the long-lived Kind-14 subscription from covering recovered keys because build_trade_key_map() only derives through the stale counter (rust/src/api/orders.rs:2520-2537).

  4. The 10-second waiter discards valid slow restores. The daemon's restore worker allows up to one hour, and #142 explicitly calls for visible progress because replay can take time. Here the client detaches after 10 seconds (rust/src/api/orders.rs:3390-3397); when RestoreData arrives later, the dispatcher removes the pending restore but drops the payload because pending.tx is None (rust/src/api/orders.rs:1780-1796). The new log makes the loss observable but does not apply any mappings or durable recovery state.

  5. The is_mine mapping is not applied retroactively. ingest_order_event consults restore mappings only while ingesting an event (rust/src/api/orders.rs:2620-2626). If the order book replayed Kind 38383 before RestoreData planted those mappings—which is the normal import-after-startup ordering—the already-cached orders remain is_mine = false; restore does not re-fetch or reprocess them.

Required verification

Please add a deterministic reinstall-style test that clears local state, imports the mnemonic with recovery enabled, receives restored indices greater than 1, and proves: the counter advances, recovered trades/sessions are persisted and visible, subscriptions cover their keys, and a delayed RestoreData result is still applied. The two new E2E tests are ignored, hardcode a local daemon, and restore in the same process, so they do not exercise these failure modes.

Local verification on this exact head: cargo test --lib passed (107 passed, 8 ignored). CI is green, but the new recovery E2E tests are among the ignored tests.

@codaMW

codaMW commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review these are all fair, and several are exactly the gaps I want to get right rather than rush.

Agreed on the framing: this PR wires and proves the RestoreSession handshake (send/correlate/reply verified E2E against a regtest daemon), but the full recovery flow isn't end-to-end yet. Specifically:

(1) unreachable path correct; the production recover: false is intentional for now (I reverted my test-only flip). The UI wiring comes once reconstruction lands.

(2) no TradeInfo/Session reconstruction this is the open design question in the PR description. RestoredOrdersInfo gives {order_id, trade_index, status} only, so building TradeInfo/Session needs either 38383-hydration + synthesis (deriving role/step from status) or richer RestoreData. Would like a direction call here before building it.
(3) counter resync good catch, this is a real bug; processing RestoreData needs to raise trade_key_index to the max recovered index so the next order doesn't reuse a key

(4) 10s waiter vs. 1h daemon window agreed; restore needs the progress/async handling #142 calls for, not a fixed short timeout. The late-reply log I added only makes the loss visible; it needs to actually apply the result.
(5) retroactive is_mine correct, and it's the exact same-process-ordering gap my test couldn't catch; restore needs to re-fetch/reprocess already-cached orders, not only flag during ingest.

I'll add a deterministic reinstall-style test as you describe. Before I build (2)/(4), I'd value a steer on the intended reconstruction + progress model @grunch, this connects to the design question I raised. Happy to hop on a call or take it in the issue.

@ermeme

ermeme Bot commented Jul 23, 2026

Copy link
Copy Markdown

Thanks — this matches my reading, and I agree that (2) and (4) need a contract decision before more client code is built. My recommended direction is:

Reconstruction

Do not synthesize a session from status alone. Status cannot tell the client its buyer/seller role or recover the peer transport key, and a kind-38383 event may be missing, stale, expired, or unavailable from the selected relays. That makes 38383 useful for enrichment/verification, but not a reliable source of truth for recovery.

RestoreData should carry an authoritative, versioned per-trade snapshot sufficient to rebuild local state. At minimum: order_id, trade_index, current status, explicit local role, the order fields needed by TradeInfo, and the peer trade pubkey needed by Session/chat. The Mostro node identity can come from the selected node context. step can then be derived locally from the explicit (role, status) pair through one tested mapping rather than guessed from status alone. Disputes should reference the same reconstructed trade and include the solver/session data the UI needs.

Applying the final snapshot should be one idempotent recovery operation: validate the whole payload; upsert trades/sessions/disputes; persist every order_id -> trade_index mapping; raise trade_key_index to the maximum recovered index across orders and disputes; refresh gift-wrap subscriptions for those keys; then reprocess/refetch cached 38383 events so is_mine is corrected. Only after those steps succeed should recovery be marked complete. A partial DB/key update must be retryable without duplicating or corrupting state.

Progress / late replies

Treat restore as a durable background operation, not a foreground RPC with a longer timeout. Give it an explicit request_id/restore_id echoed by progress, final, and error messages; persist the pending restore locally; and let the dispatcher apply a valid final RestoreData even when no UI waiter is attached. Navigating away or restarting the app must not cause a late valid result to be discarded.

Until the daemon exposes real progress, the UI should show indeterminate progress plus truthful local phases (requesting, response received, rebuilding, resubscribing, complete) rather than a synthetic percentage. If real server-side progress is desired, add an explicit progress message/chunk contract in mostro-core/daemon.

Suggested implementation order: protocol/core + daemon snapshot contract first, then transactional/idempotent client reconstruction, then UI wiring, then the deterministic clean-install test. That test should include a recovered index greater than 1, a delayed response beyond 10 seconds, an order already present in the cache, restart during recovery, and verification that the next new trade uses a fresh index.

I would keep this PR non-mergeable for issue #142 until that path works end to end. If the goal is to merge only the handshake foundation first, I suggest splitting/re-scoping it explicitly as infrastructure and removing any partial recovery side effects that could be mistaken for completed restore behavior.

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

It works very well. There are two improvements we could make. When the user enters the words and goes to the screen where the user must confirm them, the user can see four choices. Each time the user makes a mistake, the user is told that the word is incorrect. The user could fail three times and guess the word by process of elimination. The user can repeat this until have "backed up" the seed without actually doing so.

I propose that if the user fails a second time, on each attempt the user should be redirected to the screen displaying the 12 words with a message saying the user have failed a second time and it should back up the words and start the verification process again.

In the account screen, secret words section remains the same; this needs to be modified. I've attached images of how it should look before and after the backup.

before backup

before-backup

after backup

after-backup

@grunch grunch assigned grunch and unassigned grunch Jul 23, 2026
@codaMW

codaMW commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @grunch, this is exactly the direction I needed, and the idempotent-reconstruction + durable-background-restore contract makes sense. I'll build it in the order you laid out (core/daemon snapshot contract -> transactional client reconstruction -> UI -> clean-install test).

One scoping question before I start: given the snapshot contract spans mostro-core + daemon + client + UI, would you prefer I re-scope this PR as the RestoreSession handshake infrastructure (strip the partial reconstruction side-effects so nothing looks like completed restore, merge the foundation), then build the full recovery as a follow-up series? Or keep this PR open until #142 works end-to-end?

I lean toward landing the handshake as infrastructure first so it's a clean reviewable unit, but happy either way.

Separately, thanks for the backup-verification feedback. I'll address the fail-twice -> re-show-words flow and the Account "Secret Words" section as part of this (or a small separate PR, your call).

@grunch

grunch commented Jul 24, 2026

Copy link
Copy Markdown
Member

Closing this one — not because the work was wrong, but because the review surfaced that the RestoreData contract itself has to change, so the reconstruction half would be rewritten regardless. Rather than keep a PR open that mixes a proven handshake with partial recovery side effects, I've broken #142 into one issue per landable unit so each gets its own reviewable PR.

The handshake you proved against regtest is preserved as its own issue and is unblocked — please take #215 first, it's essentially this PR minus the reconstruction side effects, and your implementation here is the reference.

Also unblocked and independent of the contract discussion: #217 (the trade_key_index reuse bug you spotted) and #219 (durable background restore — the 10s-vs-1h window).

The contract itself is #216 (mostro-core + mostrod); everything heavy (#218, #220, #221, #222) waits on it.

Separately, #223 is the backup-verification UX (fail-twice -> re-show words, Account "Secret Words") — split out so it isn't stuck behind any of this. Feel free to take it in parallel.

Full breakdown and the agreed design constraints are in #142. Thanks for the E2E work here — it's what made the contract gap concrete.

@grunch grunch closed this Jul 24, 2026
codaMW added a commit to codaMW/app that referenced this pull request Jul 24, 2026
Adds widget tests covering the requested cases: first wrong pick stays on step 2
with feedback; second wrong pick on the same word returns to step 1 with the
SnackBar; after restart one wrong pick is tolerated (reset works); all correct
confirms the backup.

Review fixes:
- per-word miss budget: _failCount resets on each correct pick (was per-round);
  field doc updated, comment no longer overclaims 'brute-force'
- _backToWords() owns the counter reset (belt-and-braces with _startVerification)
- _failCount++ wrapped in setState
- stale MostroP2P#204 references corrected to MostroP2P#223

Adds @VisibleForTesting debugWords + debugCorrectWordForActiveSlot seams so the
verification flow is widget-testable without the Rust bridge. Refs MostroP2P#223
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants