4762ecf6 - Software-to-BitBox migration wizard with balance transfer - #884
4762ecf6 - Software-to-BitBox migration wizard with balance transfer#884TaprootFreak wants to merge 26 commits into
Conversation
2cf15fc to
64267a3
Compare
- authenticateLinkedAccount: POST /v1/auth for a new address carrying the current session's bearer token, so the API links the address to the same account (OptionalJwtAuthGuard); 409 surfaces as AddressAlreadyLinkedException - bearerTokenOverride on authenticatedGet/Put/Post: explicit-token calls skip the 401 refresh (a refresh would mint a token for the wrong identity) - acquireUncommittedBitboxWallet/persistBitboxWallet: uncommitted draft with id-0 sentinel, idempotent persist deduplicated by address, no current-wallet switch (mirrors the software-wallet draft/commit pair) - getRegistrationInfoWith/registerWalletFor: registration flow in an explicit token+account context for the migration wizard - buildSignMessage now mirrors the API's environment-scoped auth message ([dev]_ prefix on testnet) — auth against the dev API was broken since the API introduced env-scoped sign messages
Settings entry (software wallets only) opening a guided wizard: pair the BitBox as an uncommitted draft via the existing connect sheet, link the new address to the current account and register it in the share register (one-tap, EIP-712 on the device), transfer the full REALU balance through the unchanged gasless SendProcessCubit (software wallet stays current throughout), then persist-deduplicated wallet row switch + session swap. Wizard is resumable at every step; a missing balance read fails loud instead of silently skipping the transfer.
…test suite A definitive (non-retryable) SendProcessFailure inside the embedded transfer flow left the user stuck: disabled retry button while the transferring state also blocked the system pop. The wizard cubit now leaves the transfer flow into its own retryable failure state, and retrying runs a fresh persist-and-prepare pass (balance re-read) instead of blindly re-sending the dead intent. The register retry now restores the stored RegisterReady state first — the previous pending-retry closure was a no-op against the state guard. Success-branch context use moved ahead of the await branch (use_build_context_synchronously). Adds the full wizard test suite: cubit state machine incl. every failure branch and the zero-balance / already-registered skips, page-manager routing per state, embedded-transfer listener behaviour, settings tile visibility, responsive matrix tests and surface-catalog entries for every sticky-CTA view.
mocktail's registerFallbackValue takes no type parameters; the explicit arguments were an analyzer error (wrong_number_of_type_arguments_method).
…test The cancelPairing test asserted before the stream listener microtask ran; the sheet test tapped the ConnectBitboxView cancel button, which pops via go_router, without a GoRouter in the tree — mount the manager on a single-entry GoRouter stack (same pattern as connect_bitbox_view_test).
- BalanceService.fetchBalance: fresh fail-loud read for money-moving flows — throws on transport/non-200/parse instead of serving the stale cache the polling path may hold - session auth-token slot is now address-bound and getAuthToken discards a late auth response when the active wallet changed mid-flight, closing the race where a stale software refresh could overwrite the migrated BitBox JWT - signature cache is scoped to the exact signed message, so legacy testnet signatures invalidated by the environment-prefixed sign message fall back to a fresh sign instead of looping on rejected auth - @no-integration-test annotations on the BitBox address acquisition paths - import order fix in the registration service test
- register step is its own page + cubit (MigrateRegisterCubit, mirror of the KYC link-wallet step) per the multi-step rule; parent keeps routing and exposes draftAccount/linkedJwt fail-loud - linking now pre-checks that the software wallet is registered, always mints a fresh JWT (refreshAuthToken) so an expired bearer can never silently sign the BitBox address up as a separate account, and treats newRegistration-after-link as a retryable link failure - transfer success no longer completes the wizard directly: a settling poll re-reads the fresh source balance (fail-loud fetchBalance) and only switches wallets at zero, re-prepares a remainder transfer if the balance dropped but is nonzero, and fails closed into a resumable timeout state otherwise - embedded transfer failure branch extracted as MigrateTransferFailureView with surface-catalog entry and full responsive-matrix coverage (workaround comment removed) - pairing sheet closes itself on finish; retry() is single-flight with an immediate busy state; intro balance punctuation moved into the template
…ites The address-bound token slot change had not been carried into the brokerbot and buy-payment-info suites.
Five blockchain-api suite call sites were missed by the address-binding change; ClientException has no const constructor.
Settling remainder case now stubs a genuinely lower follow-up balance, S.current is only read after localization is pumped, the PopScope matrix targets the outer manager scope (the register step page nests its own), the malformed-JSON brokerbot group gets the wallet stub its sibling setup already had, and the auth-service suite binds cached tokens to the stub wallet address and instruments saveSignature calls explicitly.
- prepare path now uses the fail-loud fetchBalance (a swallowed refresh error can no longer surface a stale zero and finish the wizard with funds left) - settling follow-ups and retry actions run through a guarded wrapper that re-installs the retry on failure instead of stranding a busy state; the settling timer survives fallible follow-ups - close() invalidates the settling generation and finishMigration re-checks after every await, so a wizard closed mid-flight cannot half-execute the identity switch - getAuthToken builds message and request from an atomic snapshot, commits only against it and gives up after three identity changes (closes the A-B-A cache poisoning window) - BitBox addresses are EIP-55-normalized on every write and the dedup lookup compares normalized (the SDK's casing is documented as inconsistent) - register failure surface is public with catalog + matrix coverage; register step receives account/bearer via constructor instead of live parent reads; misleading injection comment corrected
fromHex rejects mixed-case input with an invalid checksum by design; uniform-case input skips the check.
healCurrentBitboxAddress now EIP-55-normalizes like the create/acquire paths (with a lowercase-device-address test); the DfxKycService injection comment no longer claims it is the smallest registered auth service; the close() comment states the actual guarantee (no rollback of completed side effects, later re-entry completes the move).
- the auth snapshot path no longer falls back to a potentially locked stale account object: an identity change during unlock now throws a file-private marker that the bounded retry loop catches with a fresh snapshot - finishMigration passes the signed message scope to saveSignature so the cached BitBox signature survives the message-scoped cache check - all three BitBox address write paths validate and normalize on the lowercased raw address, so mixed-case device output is normalized instead of rejected as unavailable
The stubs' reported wallet address and the account's actual address are compared since round 3 — bind the stub account to the fixture address, use a checksum-neutral digits-only fixture in the wire-surface group, and give the hanging-account timeout test the account's real derived address.
…th path getSignature and getAuthResponse previously delegated to the snapshot helpers without catching the file-private identity marker — a mid-unlock wallet switch would have leaked it. A shared _withIdentityRetry helper now wraps all three public entry points with the bounded fresh-snapshot retry and the readable fail-loud exception, so the marker genuinely never leaves the file.
The persisted fixture now shares the draft's address (matching persistBitboxWallet's behaviour) and buildSignMessage is stubbed on the exact address, so a signature saved under a foreign address or a wrong message argument fails the test.
f8bc49a to
da58abb
Compare
|
Review process: 6 full passes across two dimensions (conventions/quality and logic/correctness) until zero findings, each pass re-checking the fix commits of the previous one. Fixed along the way, beyond the initial implementation: a money postcondition for the transfer (settling poll on a fail-loud fresh balance read before the wallet switch, remainder re-transfer, fail-closed timeout), link hardening (fresh bearer + source-registration precheck so an expired token can never silently attach the device to a separate account), an address-bound session token with an atomic-snapshot auth retry (closes an A-B-A cache poisoning window), message-scoped signature caching (keeps testnet auth working across the env-scoped sign-message change), EIP-55 normalization on all BitBox address writes, an escape hatch for terminal transfer failures, single-flight retries, close-mid-flight guards, the register step extracted as its own page + cubit per the multi-step rule, and full responsive-matrix/catalog coverage for every new sticky-CTA surface. One deliberate product note is in the PR description: the wizard's tightly-bound self-transfer is reachable without the insider unlock that gates general Pay/Send (#885). |
The baseline commit was pushed by the actions bot, whose default-token pushes intentionally start no workflow runs.
The keep-message and drop-message branches of saveSignature's optional third argument and the settling timeout reached through consecutive poll errors were unexercised.
Eleven baselines matching the repo's per-screen golden convention: intro, register, registration pending, transfer ready/in-flight/failure, settling, settling timeout, success and both failure variants. PNGs are rendered by the self-hosted runner via golden-regenerate.
What
Adds an in-app migration wizard that moves a user from the software wallet to a BitBox hardware wallet in one guided flow, including the wallet balance:
POST /v1/auth; the API'sOptionalJwtAuthGuardattaches the address to the same account), and registers it in the share register through a dedicated register step page (EIP-712 signed on the device, mirroring the KYC link-wallet step).SendProcessCubitunchanged: the software wallet stays the app's current wallet throughout the wizard, so the existing software-only signing gate holds. The wallet row for the BitBox is persisted (deduplicated by normalized address) before any funds move.Entry point: Settings → "Move to BitBox", visible only for software wallets (same local wallet-mode gate as the seed-backup tile).
Why
Users who started on a software wallet had no in-app path to a hardware wallet — upgrading meant manual re-onboarding and moving funds by hand.
Design notes
getRegistrationInfostates 1:1; a pending/manual-review registration stops the wizard before funds move. The only local gates are wallet-mode visibility and BitBox connectivity (physical capabilities). A linking response that does not attach to the account is treated as a retryable link failure, never silently accepted.getAuthTokenbuilds message and request from an atomic wallet snapshot with a bounded identity-change retry, the cached signature is scoped to the exact signed message, andbuildSignMessagenow mirrors the API's environment-scoped auth message ([dev]_prefix on testnet) — auth against the dev API had been broken since the API introduced env-scoped sign messages.Tests
Full local gate run (codegen chain →
flutter analyze→flutter test --exclude-tags golden) on the rebased head: analyze clean, 5365 tests passed. New coverage spans the service layer (linked auth incl. header/body assertions, snapshot retry and identity-flap exhaustion, address-bound token cache, message-scoped signature cache, fail-loud balance fetch, wallet draft/persist dedup incl. casing), the complete wizard state machine (every failure branch, settling outcomes, close-mid-flight guards, single-flight retry, zero-balance and already-registered re-entries), the register step cubit, page-manager routing and PopScope matrix, settings-tile visibility per wallet mode, and responsive matrix + surface-catalog entries for every new sticky-CTA view.