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 '<word>' 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.
This commit is contained in:
jwinterm
2026-08-03 13:53:13 -04:00
parent 4dd15704b0
commit 2afcf010e1
3 changed files with 62 additions and 6 deletions
+36
View File
@@ -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<void> 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<WalletRestoreViewModel>(
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: <String, dynamic>{
'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