mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
Merge pull request 'Dev' (#39) from github-such-software/hash-wallet:dev into dev
Reviewed-on: Builds/hash-wallet#39
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -14,12 +14,25 @@ class CommonTestCases {
|
||||
String key, {
|
||||
bool shouldPumpAndSettle = true,
|
||||
int pumpDuration = 100,
|
||||
Duration timeout = const Duration(seconds: 20),
|
||||
}) async {
|
||||
await tester.pump(Duration(milliseconds: 500));
|
||||
|
||||
final widgetFinder = find.byKey(ValueKey(key));
|
||||
|
||||
expect(tester.any(widgetFinder), true, reason: 'Widget with key "$key" should be visible');
|
||||
// Wait for the widget to actually appear rather than pumping a flat 500ms
|
||||
// and hoping. Screens that arrive behind an async transition (PIN setup's
|
||||
// success button is the usual offender) are otherwise a coin flip, and the
|
||||
// failure reads as "key missing" rather than "key not there YET".
|
||||
//
|
||||
// Deliberately a pump loop, not pumpAndSettle: the dashboard runs a
|
||||
// perpetual sync-indicator animation, so settling never completes there.
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (!tester.any(widgetFinder) && DateTime.now().isBefore(deadline)) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
expect(tester.any(widgetFinder), true,
|
||||
reason: 'Widget with key "$key" should be visible '
|
||||
'(waited ${timeout.inSeconds}s)');
|
||||
|
||||
final widget = widgetFinder.first;
|
||||
await tester.tap(widget, warnIfMissed: false);
|
||||
|
||||
@@ -6,10 +6,13 @@ class CommonTestConstants {
|
||||
static final String sendTestAmount = '0.00008';
|
||||
static final String sendTestFiatAmount = '1.00';
|
||||
static final String exchangeTestAmount = '0.01';
|
||||
static final WalletType testWalletType = WalletType.solana;
|
||||
// Hash Bags is Wownero-first and no longer ships Solana, so the defaults
|
||||
// moved off SOL. WalletType.solana does not exist in this fork.
|
||||
static final WalletType testWalletType = WalletType.wownero;
|
||||
static final String testWalletName = 'Integrated Testing Wallet';
|
||||
static final CryptoCurrency sendTestReceiveCurrency = CryptoCurrency.sol;
|
||||
static final CryptoCurrency exchangeTestReceiveCurrency = CryptoCurrency.usdtSol;
|
||||
static final CryptoCurrency exchangeTestDepositCurrency = CryptoCurrency.sol;
|
||||
static final String testWalletAddress = '7TSTXnQZxQDA4JoNqs4DqVqE7oNWk2kmHkMT6DD6vw2S';
|
||||
static final CryptoCurrency sendTestReceiveCurrency = CryptoCurrency.wow;
|
||||
static final CryptoCurrency exchangeTestReceiveCurrency = CryptoCurrency.btc;
|
||||
static final CryptoCurrency exchangeTestDepositCurrency = CryptoCurrency.wow;
|
||||
static final String testWalletAddress =
|
||||
'Wo3MWeKwtA918DU4c69hVSNgejdWFCRCuWjShRY66mJkU2Hv58eygJWDJS1MNa2Ge5M1WjUkGHJLAjaZYLBZnDh42Q2u1Es9J';
|
||||
}
|
||||
|
||||
@@ -352,30 +352,21 @@ class CommonTestFlows {
|
||||
return secrets.bitcoinCashTestWalletSeeds;
|
||||
case WalletType.polygon:
|
||||
return secrets.polygonTestWalletSeeds;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletSeeds;
|
||||
case WalletType.base:
|
||||
return secrets.baseTestWalletSeeds;
|
||||
case WalletType.arbitrum:
|
||||
return secrets.arbitrumTestWalletSeeds;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletSeeds;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletSeeds;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletSeeds;
|
||||
case WalletType.wownero:
|
||||
return secrets.wowneroTestWalletSeeds;
|
||||
case WalletType.zano:
|
||||
return secrets.zanoTestWalletSeeds;
|
||||
case WalletType.decred:
|
||||
return secrets.decredTestWalletSeeds;
|
||||
// Hash Bags does not ship SOL/TRX/ZEC/DCR/ZANO/Haven — those WalletType
|
||||
// members no longer exist. dogecoin ships, but has no test seed in
|
||||
// .secrets.g.dart.
|
||||
case WalletType.dogecoin:
|
||||
return secrets.dogeTestWalletSeeds;
|
||||
case WalletType.zcash:
|
||||
return secrets.zcashTestWalletSeeds;
|
||||
case WalletType.none:
|
||||
case WalletType.haven:
|
||||
case WalletType.banano:
|
||||
throw Exception("Unable to get seeds for ${walletType}");
|
||||
}
|
||||
@@ -402,10 +393,6 @@ class CommonTestFlows {
|
||||
return secrets.arbitrumTestWalletReceiveAddress;
|
||||
case WalletType.bsc:
|
||||
return secrets.bscTestWalletReceiveAddress;
|
||||
case WalletType.solana:
|
||||
return secrets.solanaTestWalletReceiveAddress;
|
||||
case WalletType.tron:
|
||||
return secrets.tronTestWalletReceiveAddress;
|
||||
case WalletType.nano:
|
||||
return secrets.nanoTestWalletReceiveAddress;
|
||||
case WalletType.wownero:
|
||||
|
||||
@@ -64,9 +64,9 @@ class WalletKeysAndSeedPageRobot {
|
||||
tester.printToConsole('$walletName wallet has seeds properly displayed');
|
||||
}
|
||||
|
||||
if (isEVMCompatibleChain(walletType) ||
|
||||
walletType == WalletType.solana ||
|
||||
walletType == WalletType.tron) {
|
||||
// Hash Bags does not ship Solana or Tron; those WalletType members were
|
||||
// removed with the chains, so only the EVM chains remain here.
|
||||
if (isEVMCompatibleChain(walletType)) {
|
||||
if (hasSeed) {
|
||||
final seedWords = appStore.wallet!.seed!.split(" ");
|
||||
for (var seedWord in seedWords) {
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||
DEVELOPMENT_TEAM = D8PL9F7X33;
|
||||
DISABLED_ARCHS = x86_64;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
@@ -640,7 +640,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||
DEVELOPMENT_TEAM = D8PL9F7X33;
|
||||
DISABLED_ARCHS = x86_64;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
@@ -684,7 +684,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||
DEVELOPMENT_TEAM = D8PL9F7X33;
|
||||
DISABLED_ARCHS = x86_64;
|
||||
ENABLE_BITCODE = NO;
|
||||
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
app_identifier("com.suchsoftware.hashwallet")
|
||||
team_id("32J6BB6VUS") # Apple Developer Team (Such Software LLC)
|
||||
team_id("D8PL9F7X33") # Apple Developer Team (Such Software LLC)
|
||||
# apple_id("you@such.software") # fill in only if you later use fastlane to upload
|
||||
|
||||
@@ -10,8 +10,40 @@ class KeyService {
|
||||
Future<String> getWalletPassword({required String walletName}) async {
|
||||
final key =
|
||||
generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
||||
final encodedPassword = await _secureStorage.read(key: key);
|
||||
return decodeWalletPassword(password: encodedPassword!);
|
||||
|
||||
// Primary read (current iOS Keychain accessibility).
|
||||
var encodedPassword = await _secureStorage.read(key: key);
|
||||
|
||||
// Fallback 1: the legacy read path queries the OTHER iOS Keychain
|
||||
// accessibility. A wallet whose password item was written by an older
|
||||
// build with different Keychain params reads back null on the primary
|
||||
// path — this recovers it. That is the "corrupted seeds after a TestFlight
|
||||
// update" failure (Wownero/BTC/etc.: wallet files intact, Keychain entry
|
||||
// unreadable under the new params).
|
||||
encodedPassword ??= await _secureStorage.readNoIOptions(key: key);
|
||||
|
||||
// Fallback 2: the backup password the Monero password migration saves
|
||||
// under a side key BEFORE it re-encrypts, in case the main save did not
|
||||
// persist (see WalletLoadingService.updateMoneroWalletPassword).
|
||||
if (encodedPassword == null) {
|
||||
final bakKey = generateStoreKeyFor(
|
||||
key: SecretStoreKey.moneroWalletPassword,
|
||||
walletName: '#__${walletName}_bak__#');
|
||||
encodedPassword = await _secureStorage.read(key: bakKey) ??
|
||||
await _secureStorage.readNoIOptions(key: bakKey);
|
||||
}
|
||||
|
||||
// Fail with a clear, CATCHABLE error instead of a null-check crash, so the
|
||||
// loader can surface the seed/restore recovery path rather than the app
|
||||
// hard-crashing ("crashed and wouldn't let me in").
|
||||
if (encodedPassword == null) {
|
||||
throw Exception(
|
||||
'Wallet password not found in secure storage for "$walletName". '
|
||||
'The keychain entry may have been lost across an app update; '
|
||||
'restore this wallet from its seed.');
|
||||
}
|
||||
|
||||
return decodeWalletPassword(password: encodedPassword);
|
||||
}
|
||||
|
||||
Future<void> saveWalletPassword({required String walletName, required String password}) async {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/core/new_wallet_arguments.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/load_current_wallet.dart';
|
||||
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.
|
||||
///
|
||||
/// Compile-time const AND gated on [kDebugMode], so in a release build both
|
||||
/// halves fold to false and every demo path below is tree-shaken out. It is
|
||||
/// impossible to enable DEMO_MODE in a store binary.
|
||||
///
|
||||
/// Enable for a local capture build with:
|
||||
/// flutter run --dart-define=DEMO_MODE=true
|
||||
///
|
||||
/// SCREENSHOT_MODE is honored as an alias so the iOS fastlane snapshot UITest
|
||||
/// (ios/RunnerUITests/HashBagsScreenshotsUITests.swift, which already passes
|
||||
/// --dart-define=SCREENSHOT_MODE=true) drives this same path unchanged.
|
||||
const bool _demoModeDefine = bool.fromEnvironment('DEMO_MODE');
|
||||
const bool _screenshotModeDefine = bool.fromEnvironment('SCREENSHOT_MODE');
|
||||
bool get isDemoMode => kDebugMode && (_demoModeDefine || _screenshotModeDefine);
|
||||
|
||||
/// PIN the demo build "unlocks" with.
|
||||
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).
|
||||
///
|
||||
/// First launch: create a fresh Wownero wallet through the real new-wallet
|
||||
/// view model — so walletInfo, derivation, paths, persistence and
|
||||
/// changeCurrentWallet all happen exactly as they do in the UI flow, just
|
||||
/// without the screens. No seed import, no network sync: instant, deterministic,
|
||||
/// zero balance. Later launches: reuse the wallet already on disk.
|
||||
///
|
||||
/// This is a THROWAWAY wallet. Never fund it. To shoot balances/history, point
|
||||
/// a demo build at a funded wallet instead (out of scope here).
|
||||
Future<void> maybeSetupDemoWallet() async {
|
||||
if (!isDemoMode) return;
|
||||
|
||||
try {
|
||||
final authStore = getIt.get<AuthenticationStore>();
|
||||
final authService = getIt.get<AuthService>();
|
||||
final prefs = getIt.get<SharedPreferences>();
|
||||
|
||||
await authService.setPassword(_demoPin);
|
||||
|
||||
final existing = prefs.getString(PreferencesKey.currentWalletName);
|
||||
if (existing != null) {
|
||||
// A demo wallet already exists from a prior run — just load it.
|
||||
await loadCurrentWallet();
|
||||
authStore.allowed();
|
||||
printV('[demo] loaded existing wallet "$existing"');
|
||||
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
|
||||
// AuthenticationStore.allowedCreate() on success.
|
||||
final vm = getIt.get<WalletNewVM>(
|
||||
param1: NewWalletArguments(type: WalletType.wownero));
|
||||
vm.name = _demoWalletName;
|
||||
await vm.create(options: <dynamic>['English', MoneroSeedType.polyseed]);
|
||||
|
||||
if (vm.state is! ExecutedSuccessfullyState) {
|
||||
throw Exception('wallet create did not finish (${vm.state.runtimeType})');
|
||||
}
|
||||
printV('[demo] created fresh Wownero wallet "$_demoWalletName"');
|
||||
} catch (e, s) {
|
||||
// Never let a demo-only path crash the app; fall back to normal onboarding
|
||||
// so the failure is visible rather than fatal.
|
||||
printV('[demo] setup failed, falling back to onboarding: $e\n$s');
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -13,6 +13,7 @@ import 'package:hash_wallet/core/trade_monitor.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/contact.dart';
|
||||
import 'package:hash_wallet/entities/default_settings_migration.dart';
|
||||
import 'package:hash_wallet/entities/demo_mode.dart';
|
||||
import 'package:hash_wallet/entities/get_encryption_key.dart';
|
||||
import 'package:hash_wallet/entities/language_service.dart';
|
||||
import 'package:hash_wallet/entities/template.dart';
|
||||
@@ -162,6 +163,11 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
|
||||
ProxyWrapper.logger = MemoryProxyLogger();
|
||||
}
|
||||
|
||||
// Capture/demo builds only (kDebugMode + --dart-define=DEMO_MODE): come up
|
||||
// straight into an unlocked wallet so screenshots/video never depend on the
|
||||
// onboarding/PIN flow. No-op in release.
|
||||
await maybeSetupDemoWallet();
|
||||
|
||||
// Basically when we're running a test
|
||||
if (topLevelKey != null) {
|
||||
runApp(
|
||||
@@ -376,11 +382,13 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
final settingsStore = appStore.settingsStore;
|
||||
final statusBarColor = Colors.transparent;
|
||||
final authenticationStore = getIt.get<AuthenticationStore>();
|
||||
final initialRoute = authenticationStore.state == AuthenticationState.uninitialized
|
||||
? Routes.welcome
|
||||
: settingsStore.currentBuiltinTor
|
||||
? Routes.startTor
|
||||
: Routes.login;
|
||||
final initialRoute = isDemoMode
|
||||
? Routes.dashboard
|
||||
: authenticationStore.state == AuthenticationState.uninitialized
|
||||
? Routes.welcome
|
||||
: settingsStore.currentBuiltinTor
|
||||
? Routes.startTor
|
||||
: Routes.login;
|
||||
final currentTheme = appStore.themeStore.currentTheme;
|
||||
final statusBarBrightness =
|
||||
currentTheme.type == currentTheme.isDark ? Brightness.light : Brightness.dark;
|
||||
|
||||
+21
-5
@@ -94,12 +94,28 @@ no captions, no title/CTA cards, no music bed. `reel.py` also enforces
|
||||
Apple's 15–30s duration window and will fail rather than emit an out-of-spec
|
||||
preview.
|
||||
|
||||
## Store sizing
|
||||
## The capture AVD (both details matter)
|
||||
|
||||
The harness creates its own AVD at exactly **1080×1920 (16:9)**. Do not point
|
||||
it at the stock "Medium Phone" AVD: that is 1080×2400 = **2.22:1**, and Google
|
||||
Play rejects phone screenshots above **2:1**. `marks.py shots` warns if the
|
||||
frames it extracts violate that ratio.
|
||||
Create it as **API 35**, at exactly **1080×1920**:
|
||||
|
||||
```bash
|
||||
avdmanager create avd -n HashBags_Capture \
|
||||
-k "system-images;android-35;google_apis;x86_64" -d pixel_6
|
||||
# then in ~/.android/avd/HashBags_Capture.avd/config.ini:
|
||||
# hw.lcd.width=1080 hw.lcd.height=1920 hw.lcd.density=420
|
||||
# hw.ramSize=6144 showDeviceFrame=no
|
||||
```
|
||||
|
||||
**Size:** the stock "Medium Phone" AVD is 1080×2400 = **2.22:1**, and Play
|
||||
rejects phone screenshots above **2:1**. `marks.py shots` warns if extracted
|
||||
frames violate that ratio.
|
||||
|
||||
**API level:** use 35, not 36. On the API 36.1 image the guest MediaCodec AVC
|
||||
encoder is broken — `screenrecord` fails with `Encoder failed (err=-38)` at
|
||||
every resolution and writes a 0-byte file, and `screencap` aborts on a
|
||||
SwiftShader DMA assertion. Verified broken under `swiftshader_indirect` *and*
|
||||
under `-gpu host` on a real NVIDIA GPU. API 35 + `-gpu software`
|
||||
+ `-feature -Vulkan` (what `capture.sh boot` uses) works.
|
||||
|
||||
iOS App Store screenshots and the App Preview still have to come from a Mac —
|
||||
see `store/SCREENSHOTS.md` for the fastlane snapshot path.
|
||||
|
||||
+42
-3
@@ -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}"
|
||||
@@ -45,6 +49,10 @@ docker run --rm \
|
||||
-e SKIP_DEPS="$SKIP_DEPS" \
|
||||
-e BUILD_MODE="$BUILD_MODE" \
|
||||
-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 '
|
||||
@@ -133,6 +141,18 @@ if [[ "$SKIP_DEPS" != "1" ]]; then
|
||||
elif [[ -x scripts/android/build_reown_deps.sh ]]; then
|
||||
echo "uniffi bindings missing -- generating via build_reown_deps.sh"
|
||||
echo "(this needs the Rust/uniffi toolchain and is slow on a cold run)"
|
||||
# The prebuilt tarball unpacks a SHALLOW git repo. prepare_reown.sh then
|
||||
# does `git fetch -a && git checkout <pinned sha>`, which cannot reach that
|
||||
# commit through a shallow history and dies with
|
||||
# fatal: unable to read tree (8a6d79ef...)
|
||||
# Unshallow in place rather than re-cloning: the tarball also carries
|
||||
# prebuilt native artifacts that a bare clone would not have.
|
||||
if [[ -d scripts/reown_flutter/.git ]] && \
|
||||
[[ "$(git -C scripts/reown_flutter rev-parse --is-shallow-repository)" == "true" ]]; then
|
||||
echo "unshallowing scripts/reown_flutter so the pinned commit is reachable"
|
||||
git -C scripts/reown_flutter fetch --unshallow --tags || \
|
||||
git -C scripts/reown_flutter fetch --depth=2147483647 --tags || true
|
||||
fi
|
||||
./scripts/android/build_reown_deps.sh || {
|
||||
echo
|
||||
echo "FATAL: could not generate the reown uniffi bindings."
|
||||
@@ -200,9 +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"
|
||||
flutter build apk --dart-define-from-file=env.json --$BUILD_MODE $EXTRA
|
||||
# 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.
|
||||
# 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}")
|
||||
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
|
||||
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:"
|
||||
|
||||
+83
-26
@@ -61,6 +61,9 @@ SCENARIOS=(core_store_set onboarding_create restore_from_seed swap_and_buy)
|
||||
# On-device path for the in-flight recording.
|
||||
DEV_CAPTURE=/data/local/tmp/hb-capture.mp4
|
||||
|
||||
# Package under capture — used to detect when the app is actually on screen.
|
||||
APP_PKG="${DEMO_APP_PKG:-com.suchsoftware.hashwallet}"
|
||||
|
||||
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
@@ -85,18 +88,40 @@ set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
|
||||
# ---------------------------------------------------------------------------
|
||||
# device helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wait up to `timeout` seconds for a device to appear. A freshly launched
|
||||
# emulator takes appreciably longer than a fixed sleep to register with adb,
|
||||
# and the port varies (5554, 5556, ...) depending on what else is running.
|
||||
# Wait up to `timeout` seconds for OUR device to appear.
|
||||
#
|
||||
# Deliberately matches on AVD name rather than taking the first device adb
|
||||
# lists: this box routinely has other capture emulators running (VeganIQ etc),
|
||||
# and grabbing the wrong one silently records somebody else's app. Falls back
|
||||
# to the sole device only when exactly one is attached.
|
||||
detect_serial() {
|
||||
[[ -n "$SERIAL" ]] && return 0
|
||||
local timeout="${1:-90}" waited=0
|
||||
local timeout="${1:-90}" waited=0 cand name
|
||||
while (( waited < timeout )); do
|
||||
SERIAL="$("$ADB" devices | awk '$2=="device" {print $1; exit}')"
|
||||
[[ -n "$SERIAL" ]] && { log "device: $SERIAL"; return 0; }
|
||||
while read -r cand; do
|
||||
[[ -z "$cand" ]] && continue
|
||||
name="$("$ADB" -s "$cand" emu avd name 2>/dev/null | head -1 | tr -d '\r')"
|
||||
if [[ "$name" == "$AVD" ]]; then
|
||||
SERIAL="$cand"; log "device: $SERIAL (avd $AVD)"; return 0
|
||||
fi
|
||||
done < <("$ADB" devices | awk '$2=="device" {print $1}')
|
||||
|
||||
# Exactly one device and it is not ours by name (physical phone, say) —
|
||||
# take it, since there is no ambiguity to get wrong.
|
||||
local all count
|
||||
all="$("$ADB" devices | awk '$2=="device" {print $1}')"
|
||||
count="$(printf '%s\n' "$all" | grep -c . || true)"
|
||||
if [[ "$count" == "1" ]]; then
|
||||
SERIAL="$(printf '%s\n' "$all" | head -1)"
|
||||
log "device: $SERIAL (only device attached)"; return 0
|
||||
fi
|
||||
|
||||
sleep 2; waited=$((waited + 2))
|
||||
done
|
||||
die "no device appeared within ${timeout}s. Run: tools/demo/capture.sh boot"
|
||||
die "no device matching AVD '$AVD' appeared within ${timeout}s.
|
||||
Attached devices:
|
||||
$("$ADB" devices | tail -n +2)
|
||||
Run 'tools/demo/capture.sh boot', or pass --serial <name> explicitly."
|
||||
}
|
||||
|
||||
adbs() { "$ADB" -s "$SERIAL" "$@"; }
|
||||
@@ -117,14 +142,21 @@ cmd_boot() {
|
||||
log "a device is already booted"; detect_serial; return 0
|
||||
fi
|
||||
log "booting AVD $AVD (headless)"
|
||||
# -feature -GLDMA: with swiftshader_indirect on a headless host, the guest
|
||||
# graphics stack otherwise aborts on
|
||||
# "Assertion failed: !rcEnc->featureInfo()->hasReadColorBufferDma"
|
||||
# which kills screencap and screenrecord — i.e. exactly the two things this
|
||||
# harness needs. Disabling the DMA colour-buffer path costs a little capture
|
||||
# throughput and makes both work.
|
||||
# These exact flags matter — headless capture is fussy and most combinations
|
||||
# silently produce nothing:
|
||||
#
|
||||
# -gpu software swiftshader_indirect aborts the guest graphics stack
|
||||
# with "Assertion failed: !rcEnc->featureInfo()
|
||||
# ->hasReadColorBufferDma", killing screencap.
|
||||
# -feature -Vulkan without it the guest MediaCodec AVC encoder fails with
|
||||
# "Encoder failed (err=-38)" at every resolution, so
|
||||
# screenrecord writes a 0-byte file.
|
||||
#
|
||||
# The AVD must also be an **API 35** image. On API 36.1 the encoder is broken
|
||||
# regardless of GPU flags — verified against both swiftshader_indirect and
|
||||
# -gpu host on a real NVIDIA GPU. Same recipe as VeganIQ_API35_Capture.
|
||||
nohup "$EMULATOR_BIN" -avd "$AVD" -no-window -no-audio -no-boot-anim \
|
||||
-gpu swiftshader_indirect -feature -GLDMA -no-snapshot-save \
|
||||
-gpu software -feature -Vulkan -no-snapshot -no-snapshot-save \
|
||||
> "$OUT_ROOT/emulator.log" 2>&1 &
|
||||
sleep 5
|
||||
detect_serial
|
||||
@@ -193,19 +225,37 @@ cmd_record() {
|
||||
fi
|
||||
local send_addr="${DEMO_SEND_ADDRESS:-}"
|
||||
|
||||
local dev_epoch_start
|
||||
dev_epoch_start="$(adbs shell date +%s%3N | tr -d '\r')"
|
||||
|
||||
log "starting screenrecord"
|
||||
# /data/local/tmp, not /sdcard: the emulator's emulated-storage FUSE layer
|
||||
# is flaky headless ("Transport endpoint is not connected"), and screenrecord
|
||||
# then fails to open its output. /data/local/tmp is a plain filesystem that
|
||||
# adb can always read and write.
|
||||
# Arm the recorder to fire the moment the app hits the foreground, NOT now.
|
||||
#
|
||||
# `flutter drive` compiles an instrumented APK before it installs anything,
|
||||
# which can take many minutes. screenrecord caps at 180s, so starting it here
|
||||
# would burn the entire budget filming the launcher and stop before the app
|
||||
# ever appeared. Poll for the package becoming the resumed activity, then
|
||||
# start recording and stamp the device clock at that instant.
|
||||
#
|
||||
# /data/local/tmp, not /sdcard: the emulator's emulated-storage FUSE layer is
|
||||
# flaky headless ("Transport endpoint is not connected") and screenrecord
|
||||
# then cannot open its output.
|
||||
adbs shell rm -f "$DEV_CAPTURE" || true
|
||||
adbs shell screenrecord --size 1080x1920 --bit-rate 12000000 \
|
||||
--time-limit 180 "$DEV_CAPTURE" &
|
||||
local stamp_file="$RAW_DIR/$scenario.recstart"
|
||||
rm -f "$stamp_file"
|
||||
|
||||
(
|
||||
# Wait for the app to be foregrounded (cap the wait so a failed build does
|
||||
# not leave this poller alive forever).
|
||||
for _ in $(seq 1 900); do
|
||||
if "$ADB" -s "$SERIAL" shell dumpsys activity activities 2>/dev/null \
|
||||
| grep -q "topResumedActivity.*$APP_PKG"; then
|
||||
"$ADB" -s "$SERIAL" shell date +%s%3N | tr -d '\r' > "$stamp_file"
|
||||
"$ADB" -s "$SERIAL" shell screenrecord --size 1080x1920 \
|
||||
--bit-rate 12000000 --time-limit 180 "$DEV_CAPTURE"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
local rec_pid=$!
|
||||
sleep 2 # let the encoder actually start before the app launches
|
||||
log "recorder armed — will start when $APP_PKG reaches the foreground"
|
||||
|
||||
log "driving scenario: $scenario"
|
||||
set +e
|
||||
@@ -222,6 +272,8 @@ cmd_record() {
|
||||
--target=integration_test/demo/scenarios/'"$scenario"'.dart \
|
||||
-d "$SERIAL" \
|
||||
--dart-define-from-file=env.json \
|
||||
--dart-define=DEMO_MODE=true \
|
||||
--dart-define=DEMO_PIN='"${DEMO_PIN:-0801}"' \
|
||||
--dart-define=DEMO_WALLET_SEED="$DEMO_SEED" \
|
||||
--dart-define=DEMO_SEND_ADDRESS="$DEMO_SEND_ADDR"
|
||||
' 2>&1 | tee "$RAW_DIR/$scenario.drive.log"
|
||||
@@ -230,9 +282,15 @@ cmd_record() {
|
||||
|
||||
log "stopping screenrecord"
|
||||
adbs shell pkill -INT screenrecord || true
|
||||
kill "$rec_pid" 2>/dev/null || true # in case it never armed
|
||||
wait "$rec_pid" 2>/dev/null || true
|
||||
sleep 3 # screenrecord needs a moment to finalise the moov atom
|
||||
|
||||
if [[ ! -s "$stamp_file" ]]; then
|
||||
warn "the recorder never armed — $APP_PKG never reached the foreground."
|
||||
warn "That usually means the build or install failed; see $RAW_DIR/$scenario.drive.log"
|
||||
fi
|
||||
|
||||
adbs pull "$DEV_CAPTURE" "$RAW_DIR/$scenario.mp4" >/dev/null \
|
||||
|| die "could not pull recording — did screenrecord start?"
|
||||
adbs shell rm -f "$DEV_CAPTURE" || true
|
||||
@@ -244,7 +302,6 @@ cmd_record() {
|
||||
else
|
||||
warn "no driver output — captions and screenshots will be unavailable"
|
||||
fi
|
||||
echo "$dev_epoch_start" > "$RAW_DIR/$scenario.recstart"
|
||||
|
||||
exit_demo_mode
|
||||
if [[ $drive_rc -ne 0 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user