From 2afcf010e1aeeb265efba62471d9411ee1ea2d44 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 3 Aug 2026 13:37:47 -0400 Subject: [PATCH] feat(demo): funded-wallet restore + build-arg fixes Adds DEMO_WALLET_SEED/DEMO_WALLET_HEIGHT support so a demo build restores a fixed (throwaway, funded) Wownero wallet instead of creating a fresh one, via WalletRestoreViewModel.create. Balance lives on-chain against the seed, so it survives any emulator wipe and is reproducible. Three real bugs fixed on the way: - build-apk.sh built the --dart-define args as a word-split string; the 16-word seed's internal spaces split into separate argv tokens and flutter treated a seed word as a build target ("Target file '' not found"). Now a bash array keeps each --dart-define one token. - WalletRestoreViewModel.getCredentials reads name/passphrase from the options MAP (not the vm field) and casts name as non-null String, so the restore options must include 'name' and 'passphrase'. - forward DEMO_WALLET_SEED/HEIGHT into the build container; source demo.env. Verified end to end: demo build restores the wallet, its address matches the funded one exactly (QR-decoded), it syncs at height 861800, and the dashboard shows the real 13.37 WOW balance + tx history. Send shows Max 13.37. The seed lives only in gitignored tools/demo/demo.env (never committed). docs/internal-notes.md (gitignored) documents the FailedDecryption gotcha. --- .gitignore | 3 +++ lib/entities/demo_mode.dart | 36 ++++++++++++++++++++++++++++++++++++ tools/demo/build-apk.sh | 29 +++++++++++++++++++++++------ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 10fcf303b..6fa4f20f8 100644 --- a/.gitignore +++ b/.gitignore @@ -244,3 +244,6 @@ scripts/windows/setup-windows-runner.ps1 # committed template is tools/demo/demo.env.example. tools/demo/demo.env __pycache__/ + +# Internal engineering notes (not for the public repo) +docs/internal-notes.md diff --git a/lib/entities/demo_mode.dart b/lib/entities/demo_mode.dart index bd8549593..8a21cc1ec 100644 --- a/lib/entities/demo_mode.dart +++ b/lib/entities/demo_mode.dart @@ -10,6 +10,7 @@ import 'package:hash_wallet/entities/preferences_key.dart'; import 'package:hash_wallet/entities/seed_type.dart'; import 'package:hash_wallet/store/authentication_store.dart'; import 'package:hash_wallet/view_model/wallet_new_vm.dart'; +import 'package:hash_wallet/view_model/wallet_restore_view_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Whether this build was launched in capture/demo mode. @@ -34,6 +35,17 @@ const String _demoPin = String.fromEnvironment('DEMO_PIN', defaultValue: '0801') /// Name of the throwaway wallet the demo build creates on first launch. const String _demoWalletName = 'Demo Wallet'; +/// Optional fixed seed. When set, the demo build RESTORES this exact wallet +/// instead of creating a fresh one — so a funded throwaway wallet is +/// reproducible and its balance (which lives on-chain against the seed) +/// survives any emulator wipe. THROWAWAY only; never a real wallet. Supplied +/// via --dart-define, kept out of git (tools/demo/demo.env). +const String _demoSeed = String.fromEnvironment('DEMO_WALLET_SEED'); + +/// Restore height for the seed above. Set it near the wallet's creation block +/// so sync is fast; 0 scans from genesis (slow). +const int _demoHeight = int.fromEnvironment('DEMO_WALLET_HEIGHT', defaultValue: 0); + /// Bring the app up straight into a usable, unlocked wallet so the capture /// harness never has to drive onboarding or the PIN pad (whose synthetic taps /// are unreliable under the headless emulator). @@ -65,6 +77,30 @@ Future maybeSetupDemoWallet() async { return; } + if (_demoSeed.trim().isNotEmpty) { + // Reproducible funded wallet: restore the fixed seed. WalletRestoreView + // Model shares WalletCreationVM.create, so it builds walletInfo/paths and + // finalizes (changeCurrentWallet + allowedCreate) just like the new-wallet + // flow. + final vm = getIt.get( + param1: WalletType.wownero, param2: null); + vm.name = _demoWalletName; + // getCredentials() reads name/passphrase FROM the options map (not the vm + // field), and casts name as a non-null String — so it must be present. + await vm.create(options: { + 'name': _demoWalletName, + 'seed': _demoSeed.trim(), + 'height': _demoHeight, + 'passphrase': '', + }); + if (vm.state is! ExecutedSuccessfullyState) { + throw Exception('wallet restore did not finish (${vm.state.runtimeType})'); + } + printV('[demo] restored Wownero wallet from DEMO_WALLET_SEED ' + '(height $_demoHeight)'); + return; + } + // Fresh Wownero wallet via the real create flow. WalletNewVM.create builds // the WalletInfo, derivation, and paths, creates + saves the wallet, calls // AppStore.changeCurrentWallet (persisting currentWalletName/type) and diff --git a/tools/demo/build-apk.sh b/tools/demo/build-apk.sh index e8fbe281d..9b3b017f9 100755 --- a/tools/demo/build-apk.sh +++ b/tools/demo/build-apk.sh @@ -19,6 +19,10 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Pull in local capture config (DEMO_WALLET_SEED/HEIGHT/PIN). Gitignored. +_DEMO_ENV="$(dirname "${BASH_SOURCE[0]}")/demo.env" +if [[ -f "$_DEMO_ENV" ]]; then set -a; . "$_DEMO_ENV"; set +a; fi IMAGE="${CI_IMAGE:-ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly}" BUILD_MODE="${BUILD_MODE:-debug}" ABI="${ABI:-x86_64}" @@ -47,6 +51,8 @@ docker run --rm \ -e TARGET_PLATFORM="$TARGET_PLATFORM" \ -e DEMO="${DEMO:-0}" \ -e DEMO_PIN="${DEMO_PIN:-0801}" \ + -e DEMO_WALLET_SEED="${DEMO_WALLET_SEED:-}" \ + -e DEMO_WALLET_HEIGHT="${DEMO_WALLET_HEIGHT:-0}" \ -e HOST_UID="$HOST_UID" \ -e HOST_GID="$HOST_GID" \ "$IMAGE" bash -c ' @@ -214,17 +220,28 @@ step "compile svg assets" ./compile_graphics.sh step "build apk ($BUILD_MODE / ${TARGET_PLATFORM:-universal})" -EXTRA="" -[[ -n "$TARGET_PLATFORM" ]] && EXTRA="--target-platform=$TARGET_PLATFORM" # DEMO=1 bakes in DEMO_MODE so the app boots straight to an unlocked demo # wallet (kDebugMode-gated, impossible in release). Lets capture skip the # onboarding/PIN flow entirely. -DEMO_DEFINES="" +# Use an ARRAY, not a string: the seed contains spaces (16 words), so a +# word-split string turns "--dart-define=DEMO_WALLET_SEED=word1 word2 ..." +# into separate argv tokens and flutter treats "unlock" as a target file +# ("Target file 'unlock' not found."). Array elements preserve the spaces. +DEMO_DEFINES=() if [[ "${DEMO:-0}" == "1" ]]; then - DEMO_DEFINES="--dart-define=DEMO_MODE=true --dart-define=DEMO_PIN=${DEMO_PIN:-0801}" - echo "DEMO_MODE baked in" + DEMO_DEFINES+=(--dart-define=DEMO_MODE=true "--dart-define=DEMO_PIN=${DEMO_PIN:-0801}") + if [[ -n "${DEMO_WALLET_SEED:-}" ]]; then + DEMO_DEFINES+=("--dart-define=DEMO_WALLET_SEED=${DEMO_WALLET_SEED}") + DEMO_DEFINES+=("--dart-define=DEMO_WALLET_HEIGHT=${DEMO_WALLET_HEIGHT:-0}") + echo "DEMO_MODE baked in (restore from seed, height ${DEMO_WALLET_HEIGHT:-0})" + else + echo "DEMO_MODE baked in (fresh wallet)" + fi fi -flutter build apk --dart-define-from-file=env.json --$BUILD_MODE $EXTRA $DEMO_DEFINES +EXTRA_ARGS=() +[[ -n "$TARGET_PLATFORM" ]] && EXTRA_ARGS+=("--target-platform=$TARGET_PLATFORM") +flutter build apk --dart-define-from-file=env.json --$BUILD_MODE \ + "${EXTRA_ARGS[@]}" "${DEMO_DEFINES[@]+"${DEMO_DEFINES[@]}"}" echo echo "==> artifacts:"