Skip to content

Mainnet migration notice for wallets coming from testnet - #649

Merged
n13 merged 8 commits into
mainfrom
n13/mainnet-migration-screens
Sep 10, 2026
Merged

Mainnet migration notice for wallets coming from testnet#649
n13 merged 8 commits into
mainfrom
n13/mainnet-migration-screens

Conversation

@n13

@n13 n13 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

When the mainnet build opens on a wallet that already exists, the user sees a one-time two-page notice instead of a silent zero balance.

Flow

  1. Intro (branded like the welcome screen): "Quantus is now on Mainnet!" with a Next button. The testnet check starts while this page is showing.
  2. Outcome, chosen from the check:
    • Miner (mined blocks on testnet): "Dear Testnet Miner", keep the wallet for the coming testnet rewards, a card with the block count, and why the mainnet balance is zero. Button: Done.
    • Holder (balance but no blocks): "Dear Testnet User", one lead line (testnet is shut down, mainnet is live, balances do not carry over), then two cards: "Mined on testnet?" keep the wallet for the coming rewards, and "Did not mine?" keep your secret phrase or create a new one. Buttons: Keep Wallet / Create New Wallet.
    • Newcomer (nothing on testnet): create a new wallet or migrate the old one, which keeps it exactly as is. Buttons: Create New Wallet / Migrate Old Wallet.
    • Testnet unreachable: the holder page opening with a warning row, "Testnet could not be reached", and a Retry that runs the check once more.

Pages live in a PageView with the eased swipe animation; swiping back to the intro works. "Create New Wallet" goes through the existing reset confirmation (backup checkbox and device auth) and lands on the welcome screen. Finishing records the outcome as a telemetry event.

Check

MainnetMigrationService holds the old Planck RPC and indexer URLs as constants and is their only user. It gathers every stored account plus each software wallet's wormhole address (where mining rewards were paid), sums account_stats.total_mined_blocks from the old indexer, and sums free balances from the old RPC. The whole check has a 20 second timeout and is not retried on its own, so the fallback page never flips back to a spinner; Retry on that page runs it again.

Gating

  • Shown when a wallet exists and mainnet_migration_done is unset; finishing sets it.
  • New wallets (create and import) set it immediately, so only upgraded installs ever see the notice. The flag write is awaited and a refused write throws: the notice stays open and shows the error, and create and import surface it through their existing error paths.
  • AppConstants.debugMainnetMigration set to any of miner, holder, newcomer, or error forces the notice in debug builds. The intro page then shows a chip per outcome, so every page can be checked by tapping, without a hot restart.

Shared card

The holder cards are the Wallet Created screen's account row, extracted to InfoCard and used in both places.

Home activity for empty accounts (pre-existing bug)

The activity list stayed on skeletons for accounts with no history until a manual refresh. The history poller reads the pagination controller and calls silentRefresh in the same tick, before the controller's own _init reaches its fetch; the initial load then joins the silent fetch through the in-flight dedup, and the silent path never clears isLoading. Accounts with rows were rescued by the non-empty check, empty ones were not. silentRefresh now runs the initial load while isLoading is set, with a regression test that fails on the old code. The unused combined all-accounts provider is removed.

SDK

SubstrateService.queryBalanceOn(rpc, address) extracted from queryBalance so the check can read balances from another chain. SettingsService gains the done flag.

Tests

Service: which addresses are checked (including the wormhole address, and none for hardware-only wallets), classification for each outcome, summing blocks across addresses, indexer failure surfacing, and the pending flag. Screen: each outcome's copy and buttons, Done and Keep and Migrate retiring the notice, Create New Wallet reaching the reset confirmation, the unreachable fallback and its Retry, a failed completion write keeping the notice open, and the loader while the check runs. Wallet creation: a failed completion write surfaces instead of adding the account. Full mobile, SDK, and cold wallet suites pass.

Simulator

Walked every page on the iPhone 17 Pro simulator with each forced outcome: intro, miner (Done to home), holder (Keep Wallet to home), newcomer (Migrate Old Wallet to home), testnet unreachable warning row with Retry, and Create New Wallet through the reset confirmation, device auth, and back to the welcome screen. The home screen shows "No Transactions Yet" right after launch for a fresh wallet.

n13 added 6 commits September 10, 2026 14:11
A wallet that already exists when the mainnet build first opens sees a
one-time two-page notice: a branded intro while the app checks the
wallet's testnet history, then a page for what it finds. Miners are told
to keep the wallet for their rewards and see their block count; users
with a balance may keep or replace the wallet; wallets with nothing on
testnet may create a new one or migrate, which keeps everything as is.

Balances come from the old Planck RPC and mined blocks from the old
indexer, both constants inside the migration service and used only
there. If testnet cannot be reached the holder page is shown with a
note. New and imported wallets mark the notice done so only upgrades
see it. debugMainnetMigration forces any outcome in debug builds.
A silent refresh fired before the controller's initial load won the
in-flight dedup and never cleared isLoading. Accounts with rows were
rescued by the non-empty check; empty accounts stayed on the skeleton
until a manual refresh. Drop the combined all-accounts provider nothing
consumed.
One lead line, then a mined and a not-mined card built from the Wallet
Created screen's row card, now shared as InfoCard.
The debug constant still forces the notice; the intro page now shows a
chip per outcome so every page can be checked without a hot restart.
Not-mined card reads: keep your secret phrase or create a new one.
The muted footnote was easy to miss. The holder page now opens with a
warning row, built from InfoCard with a new trailing slot, and a Retry
that runs the check once more. QuantusButton hugs its content when no
width is given so the underline variant works inline.

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict: changes requested.

  • [P2] Await the migration completion write before leaving the flow. SettingsService.setMainnetMigrationDone drops the Future returned by SharedPreferences.setBool, while MainnetMigrationScreen._finish immediately renders Home and the create/import paths immediately navigate onward. If the platform write fails, the app still treats the notice as retired and the failure is not surfaced; on the next launch the notice appears again, including for a newly created wallet. Make the setter asynchronous, treat a false result as a failure, await it through MainnetMigrationService.markDone and all call sites, and only transition after success. Please add a regression test for a failed completion write.

  • [P3] mobile-app/lib/v2/screens/welcome/mainnet_migration_screen.dart:288 hard-codes BorderRadius.circular(16). The mobile v2 rules require radii from context.radiusV3; use the matching theme radius token here.

Validation: the three focused migration/pagination test files pass (18 tests), GitHub Analyze and dependency checks pass, and the live testnet GraphQL response matches the id/total_mined_blocks assumptions.

n13 added 2 commits September 10, 2026 17:22
setMainnetMigrationDone now awaits SharedPreferences and throws when the
write is refused, so a wallet is never treated as migrated on a failed
write. The notice stays open and shows the error; create and import
surface it through their existing error paths. Block count card uses
the md radius token. Regression tests cover a failed write on the
notice and on wallet creation; the settings mock is regenerated.

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict: approved.

The previous findings are resolved:

  • The completion flag is now awaited, a refused SharedPreferences write throws, the migration screen stays open and reports the failure, and create/import paths await persistence.
  • The block-count card now uses the v2 theme radius token.

No new findings on the latest head ca73686.

Validation: 22 focused migration, pagination, and wallet-creation tests passed locally; the final mock-formatting commit was rechecked with its dependent test; git diff --check is clean; and the full GitHub workflow passed formatting, analysis, mobile-app tests, SDK tests, and cold-wallet tests.

@n13
n13 merged commit f9b95ee into main Sep 10, 2026
2 checks passed
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