Skip to content

4762ecf6 - Software-to-BitBox migration wizard with balance transfer - #884

Draft
TaprootFreak wants to merge 6 commits into
stagingfrom
feature/bitbox-migration
Draft

4762ecf6 - Software-to-BitBox migration wizard with balance transfer#884
TaprootFreak wants to merge 6 commits into
stagingfrom
feature/bitbox-migration

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Pair the BitBox — existing pairing sheet, but the wallet is acquired as an uncommitted draft: no row is persisted and the current wallet is not switched at this point.
  2. Link & register the new addressPOST /v1/auth for the BitBox address carrying the current session's bearer token; the API's OptionalJwtAuthGuard attaches the address to the same account, which makes the existing one-tap AddWallet registration state reachable. The share-register registration is EIP-712-signed on the device.
  3. Transfer the full REALU balance — the gasless wallet-to-wallet transfer (EIP-7702), reusing SendProcessCubit unchanged: the software wallet stays the app's current wallet throughout the wizard, so the existing software-only signing gate holds.
  4. Switch to the BitBox wallet — the wallet row is persisted (deduplicated by address) BEFORE any funds move; the final step only switches the current wallet, swaps the session token/signature, and reloads.

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

  • Resumable at every step. The server-side account link and registration are idempotent, the BitBox wallet row is persisted before the balance transfer, and a zero balance skips the transfer step. An interruption at any point leaves the funds either on the still-active software wallet or on an address the app already knows.
  • The software wallet row is intentionally kept after the move (the seed remains available as a backup); no auto-delete.
  • API is decision authority. Registration routing renders the getRegistrationInfo states 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).
  • buildSignMessage now 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; this feature depends on dev-environment testability, so the fix is included here.

Tests

Full local gate run (codegen chain → flutter analyzeflutter test --exclude-tags golden): analyze clean, 5054 tests passed. New coverage: service-layer unit tests (linked auth incl. header/body/401-no-refresh assertions, wallet draft/persist dedup, explicit-context registration, env-scoped sign message), the complete wizard cubit state machine incl. every failure branch and the zero-balance / already-registered re-entry skips, page-manager routing per state, embedded-transfer listener behaviour, settings-tile visibility per wallet mode, plus responsive matrix tests and surface-catalog entries for every new sticky-CTA view.

- 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).
@TaprootFreak
TaprootFreak force-pushed the feature/bitbox-migration branch from 2cf15fc to 64267a3 Compare August 3, 2026 16:21
- 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
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.

1 participant