From 9a904da625dc24c4579b988220375eb7747f9315 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 7 Jan 2026 13:26:17 +0100 Subject: [PATCH 01/39] formatting and small updates --- scripts/build-rn-android-apk.sh | 2 +- test/specs/send.e2e.ts | 4 +++- wdio.conf.ts | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/build-rn-android-apk.sh b/scripts/build-rn-android-apk.sh index 6c72ac2..9b15c2e 100755 --- a/scripts/build-rn-android-apk.sh +++ b/scripts/build-rn-android-apk.sh @@ -27,7 +27,7 @@ fi if [[ -z "${ENV_FILE:-}" ]]; then if [[ "$BACKEND" == "regtest" ]]; then - ENV_FILE=".env.test.template" + ENV_FILE=".env.development.template" else ENV_FILE=".env.development" fi diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index f7bbb27..e5efefc 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -356,7 +356,9 @@ describe('@send - Send', () => { await expectTextWithin('ActivitySpending', '7 000'); } else { // https://github.com/synonymdev/bitkit-ios/issues/300 - console.info('Skipping sending to unified invoice w/ expired invoice on iOS due to /bitkit-ios/issues/300'); + console.info( + 'Skipping sending to unified invoice w/ expired invoice on iOS due to /bitkit-ios/issues/300' + ); amtAfterUnified3 = amtAfterUnified2; } diff --git a/wdio.conf.ts b/wdio.conf.ts index 7f5dad1..4aaedd9 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -65,7 +65,6 @@ export const config: WebdriverIO.Config = { 'appium:deviceName': 'Pixel_6', 'appium:platformVersion': '13.0', 'appium:app': path.join(__dirname, 'aut', 'bitkit_e2e.apk'), - // 'appium:app': path.join(__dirname, 'aut', 'bitkit_v1.1.2.apk'), 'appium:autoGrantPermissions': true, } : { From 41209d792913aee6c45d6e804567ce4b01c97518 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 7 Jan 2026 13:26:51 +0100 Subject: [PATCH 02/39] basic migration scenarios for android --- test/helpers/actions.ts | 8 ++- test/helpers/setup.ts | 6 +- test/specs/migration.e2e.ts | 125 +++++++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index f1eb58b..0142fc4 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -522,14 +522,18 @@ export async function restoreWallet( { passphrase, expectQuickPayTimedSheet = false, - }: { passphrase?: string; expectQuickPayTimedSheet?: boolean } = {} + reinstall = true, + }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; reinstall?: boolean } = {} ) { console.info('→ Restoring wallet with seed:', seed); // Let cloud state flush - carried over from Detox await sleep(5000); // Reinstall app to wipe all data - await reinstallApp(); + if (reinstall) { + console.info('Reinstalling app to reset state...'); + await reinstallApp(); + } // Terms of service await elementById('Continue').waitForDisplayed(); diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 911b7a0..8c86a3b 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -30,9 +30,7 @@ export function getRnAppPath(): string { const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_rn_regtest.apk'); const appPath = process.env.RN_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { - throw new Error( - `RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}` - ); + throw new Error(`RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}`); } return appPath; } @@ -61,7 +59,7 @@ export async function reinstallAppFromPath(appPath: string, appId: string = getA * (Wallet data is stored in iOS Keychain and persists even after app uninstall * unless the whole simulator is reset or keychain is reset specifically) */ -function resetBootedIOSKeychain() { +export function resetBootedIOSKeychain() { if (!driver.isIOS) return; let udid = ''; diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 8d03450..2650aff 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,25 +1,64 @@ -import { elementById, restoreWallet, sleep, tap, typeText, waitForSetupWalletScreenFinish } from '../helpers/actions'; +import { + dismissBackupTimedSheet, + elementById, + elementByIdWithin, + expectText, + expectTextWithin, + handleAndroidAlert, + restoreWallet, + sleep, + swipeFullScreen, + tap, + typeText, + waitForSetupWalletScreenFinish, +} from '../helpers/actions'; import { ciIt } from '../helpers/suite'; -import { getNativeAppPath, getRnAppPath, reinstallAppFromPath } from '../helpers/setup'; +import { + getNativeAppPath, + getRnAppPath, + reinstallAppFromPath, + resetBootedIOSKeychain, +} from '../helpers/setup'; +import { getAppId } from '../helpers/constants'; const MIGRATION_MNEMONIC = process.env.MIGRATION_MNEMONIC ?? 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; describe('@migration - Legacy RN migration', () => { - ciIt('@migration_1 - Can restore legacy RN wallet from mnemonic', async () => { + ciIt('@migration_1 - Remove legacy RN app and install native app', async () => { await installLegacyRnApp(); await restoreLegacyRnWallet(MIGRATION_MNEMONIC); - // Restore into native app - // await installNativeApp(); - await restoreWallet(MIGRATION_MNEMONIC); + // Reinstall native app + console.info(`→ Reinstalling app from: ${getNativeAppPath()}`); + await driver.removeApp(getAppId()); + resetBootedIOSKeychain(); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // restore wallet and verify migration + await restoreWallet(MIGRATION_MNEMONIC, { reinstall: false }); + await verifyMigration(); + }); + + ciIt('@migration_2 - Install native app on top of legacy RN app', async () => { + await installLegacyRnApp(); + await restoreLegacyRnWallet(MIGRATION_MNEMONIC); + + // Install native app + console.info(`→ Installing app from: ${getNativeAppPath()}`); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // verify migration + await handleAndroidAlert(); + await expectText('Migration Complete'); + await dismissBackupTimedSheet(); + await verifyMigration(); }); }); -async function installNativeApp() { - await reinstallAppFromPath(getNativeAppPath()); -} async function installLegacyRnApp() { await reinstallAppFromPath(getRnAppPath()); } @@ -41,7 +80,73 @@ async function restoreLegacyRnWallet(seed: string) { await waitForSetupWalletScreenFinish(); const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed( { timeout: 120000 }); + await getStarted.waitForDisplayed({ timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); + await expectText(totalBalance); + await expectText(savingBalance); + await expectText(spendingBalance); +} + +const totalBalance = '141 321'; +const savingBalance = '91 766'; +const spendingBalance = '49 555'; + +async function verifyMigration() { + console.info('→ Verifying migrated wallet balances...'); + const totalBalanceEl = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); + await expect(totalBalanceEl).toHaveText(totalBalance); + await expectTextWithin('ActivitySpending', spendingBalance); + await expectTextWithin('ActivitySavings', savingBalance); + + console.info('→ Verify transaction details...'); + await swipeFullScreen('up'); + await swipeFullScreen('up'); + await tap('ActivityShowAll'); + + // All transactions + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '+'); + await expectTextWithin('Activity-3', '-'); + await expectTextWithin('Activity-4', '-'); + await expectTextWithin('Activity-5', '+'); + await expectTextWithin('Activity-6', '+'); + + // Sent, 2 transactions + await tap('Tab-sent'); + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '-'); + await expectTextWithin('Activity-3', '-'); + await elementById('Activity-4').waitForDisplayed({ reverse: true }); + + // Received, 2 transactions + await tap('Tab-received'); + await expectTextWithin('Activity-1', '+'); + await expectTextWithin('Activity-2', '+'); + await expectTextWithin('Activity-3', '+'); + await elementById('Activity-4').waitForDisplayed({ reverse: true }); + + // Other, 0 transactions + await tap('Tab-other'); + await elementById('Activity-1').waitForDisplayed(); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + + // filter by receive tag + await tap('Tab-all'); + await tap('TagsPrompt'); + await sleep(500); + await tap('Tag-received '); + await expectTextWithin('Activity-1', '+'); + await expectTextWithin('Activity-2', '+'); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); + await tap('Tag-received -delete'); + + // filter by send tag + await tap('TagsPrompt'); + await sleep(500); + await tap('Tag-sent'); + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '-'); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); + await tap('Tag-sent-delete'); } From d5fb90ace220bfd85fe0f8219f86c5ef2222bce1 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 10:19:23 +0100 Subject: [PATCH 03/39] bitkit RN build script --- README.md | 6 +++ scripts/build-rn-ios-sim.sh | 73 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100755 scripts/build-rn-ios-sim.sh diff --git a/README.md b/README.md index 19acfcf..636fcbb 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ If you have `bitkit-e2e-tests`, `bitkit-android`, and `bitkit-ios` checked out i # Legacy RN Android (builds ../bitkit and copies APK to ./aut/bitkit_rn_regtest.apk) ./scripts/build-rn-android-apk.sh +# Legacy RN iOS simulator (builds ../bitkit and copies app to ./aut/bitkit_rn_regtest_ios.app) +./scripts/build-rn-ios-sim.sh + # iOS (builds ../bitkit-ios and copies IPA to ./aut/bitkit_e2e.ipa) ./scripts/build-ios-sim.sh ``` @@ -73,6 +76,9 @@ BACKEND=regtest ./scripts/build-android-apk.sh # Legacy RN Android BACKEND=regtest ./scripts/build-rn-android-apk.sh +# Legacy RN iOS simulator +BACKEND=regtest ./scripts/build-rn-ios-sim.sh + # iOS BACKEND=local ./scripts/build-ios-sim.sh BACKEND=regtest ./scripts/build-ios-sim.sh diff --git a/scripts/build-rn-ios-sim.sh b/scripts/build-rn-ios-sim.sh new file mode 100755 index 0000000..ee3960b --- /dev/null +++ b/scripts/build-rn-ios-sim.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Build the legacy Bitkit RN iOS simulator app from ../bitkit and copy into aut/ +# +# Inputs/roots: +# - E2E root: this repo (bitkit-e2e-tests) +# - RN root: ../bitkit (resolved relative to this script) +# +# Output: +# - Copies .app -> aut/bitkit_rn_regtest_ios.app +# +# Usage: +# ./scripts/build-rn-ios-sim.sh [debug|release] +# BACKEND=regtest ./scripts/build-rn-ios-sim.sh +# ENV_FILE=.env.test.template ./scripts/build-rn-ios-sim.sh +set -euo pipefail + +E2E_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RN_ROOT="$(cd "$E2E_ROOT/../bitkit" && pwd)" + +BUILD_TYPE="${1:-debug}" +BACKEND="${BACKEND:-regtest}" + +if [[ "$BUILD_TYPE" != "debug" && "$BUILD_TYPE" != "release" ]]; then + echo "ERROR: Unsupported build type: $BUILD_TYPE (expected debug|release)" >&2 + exit 1 +fi + +if [[ -z "${ENV_FILE:-}" ]]; then + if [[ "$BACKEND" == "regtest" ]]; then + ENV_FILE=".env.development.template" + else + ENV_FILE=".env.development" + fi +fi + +if [[ ! -f "$RN_ROOT/$ENV_FILE" ]]; then + echo "ERROR: Env file not found: $RN_ROOT/$ENV_FILE" >&2 + exit 1 +fi + +echo "Building RN iOS simulator app (BACKEND=$BACKEND, ENV_FILE=$ENV_FILE, BUILD_TYPE=$BUILD_TYPE)..." + +pushd "$RN_ROOT" >/dev/null +if [[ -f .env ]]; then + cp .env .env.bak +fi +cp "$ENV_FILE" .env +E2E_TESTS=true yarn "e2e:build:ios-$BUILD_TYPE" +if [[ -f .env.bak ]]; then + mv .env.bak .env +else + rm -f .env +fi +popd >/dev/null + +if [[ "$BUILD_TYPE" == "debug" ]]; then + IOS_CONFIG="Debug" +else + IOS_CONFIG="Release" +fi + +APP_PATH="$RN_ROOT/ios/build/Build/Products/${IOS_CONFIG}-iphonesimulator/bitkit.app" +if [[ ! -d "$APP_PATH" ]]; then + echo "ERROR: iOS .app not found at: $APP_PATH" >&2 + exit 1 +fi + +OUT="$E2E_ROOT/aut" +mkdir -p "$OUT" +OUT_APP="$OUT/bitkit_rn_${BACKEND}_ios.app" +rm -rf "$OUT_APP" +cp -R "$APP_PATH" "$OUT_APP" +echo "RN iOS simulator app copied to: $OUT_APP" From f402f9df1db39ed649aa78abcdee90a495aaba54 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 10:19:58 +0100 Subject: [PATCH 04/39] adjustments for iOS --- test/helpers/actions.ts | 6 +++--- test/helpers/setup.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 0142fc4..7e44467 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -88,11 +88,11 @@ export function elementByText( } else { if (strategy === 'exact') { return $( - `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton") AND (label == "${text}" OR value == "${text}")` + `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton" OR type == "XCUIElementTypeOther") AND (label == "${text}" OR value == "${text}")` ); } return $( - `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton") AND label CONTAINS "${text}"` + `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton" OR type == "XCUIElementTypeOther") AND label CONTAINS "${text}"` ); } } @@ -567,7 +567,7 @@ export async function restoreWallet( // Wait for Get Started const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed(); + await getStarted.waitForDisplayed( { timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); await handleAndroidAlert(); diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 8c86a3b..488f0d0 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -27,7 +27,8 @@ export async function reinstallApp() { } export function getRnAppPath(): string { - const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_rn_regtest.apk'); + const appFileName = driver.isIOS ? 'bitkit_rn_regtest_ios.app' : 'bitkit_rn_regtest.apk'; + const fallback = path.join(__dirname, '..', '..', 'aut', appFileName); const appPath = process.env.RN_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { throw new Error(`RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}`); @@ -36,7 +37,8 @@ export function getRnAppPath(): string { } export function getNativeAppPath(): string { - const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_e2e.apk'); + const appFileName = driver.isIOS ? 'bitkit.app' : 'bitkit_e2e.apk'; + const fallback = path.join(__dirname, '..', '..', 'aut', appFileName); const appPath = process.env.NATIVE_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { throw new Error( From 35fbe5cf30ff31ec0c4ee602f8fc8041b300df03 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 15:02:35 +0100 Subject: [PATCH 05/39] formatting --- test/helpers/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 7e44467..c1ab4ca 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -567,7 +567,7 @@ export async function restoreWallet( // Wait for Get Started const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed( { timeout: 120000 }); + await getStarted.waitForDisplayed({ timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); await handleAndroidAlert(); From cd425e48c1001a72bb421aa9ca6055c31c7c1629 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 18:02:10 +0100 Subject: [PATCH 06/39] adjust verify tx details --- test/specs/migration.e2e.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 2650aff..93c3b52 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,5 +1,6 @@ import { dismissBackupTimedSheet, + doNavigationClose, elementById, elementByIdWithin, expectText, @@ -112,21 +113,20 @@ async function verifyMigration() { await expectTextWithin('Activity-5', '+'); await expectTextWithin('Activity-6', '+'); - // Sent, 2 transactions + // Sent await tap('Tab-sent'); await expectTextWithin('Activity-1', '-'); await expectTextWithin('Activity-2', '-'); - await expectTextWithin('Activity-3', '-'); - await elementById('Activity-4').waitForDisplayed({ reverse: true }); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); - // Received, 2 transactions + // Received await tap('Tab-received'); await expectTextWithin('Activity-1', '+'); await expectTextWithin('Activity-2', '+'); await expectTextWithin('Activity-3', '+'); await elementById('Activity-4').waitForDisplayed({ reverse: true }); - // Other, 0 transactions + // Other await tap('Tab-other'); await elementById('Activity-1').waitForDisplayed(); await elementById('Activity-2').waitForDisplayed({ reverse: true }); @@ -149,4 +149,6 @@ async function verifyMigration() { await expectTextWithin('Activity-2', '-'); await elementById('Activity-3').waitForDisplayed({ reverse: true }); await tap('Tag-sent-delete'); + + await doNavigationClose(); } From f78caad1476abc75c00dadfeeda4bf13203915f6 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 22:20:59 +0100 Subject: [PATCH 07/39] attempt to use regtest and local backend --- test/helpers/actions.ts | 53 +++++++---- test/helpers/constants.ts | 7 +- test/helpers/regtest.ts | 181 ++++++++++++++++++++++++++++++++++++ test/specs/backup.e2e.ts | 7 +- test/specs/boost.e2e.ts | 15 ++- test/specs/lightning.e2e.ts | 11 +-- test/specs/lnurl.e2e.ts | 8 +- test/specs/migration.e2e.ts | 18 ++-- test/specs/onchain.e2e.ts | 18 ++-- test/specs/security.e2e.ts | 8 +- test/specs/send.e2e.ts | 13 ++- test/specs/transfer.e2e.ts | 14 ++- 12 files changed, 270 insertions(+), 83 deletions(-) create mode 100644 test/helpers/regtest.ts diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index c1ab4ca..cdc2f68 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -1,6 +1,6 @@ import type { ChainablePromiseElement } from 'webdriverio'; import { reinstallApp } from './setup'; -import BitcoinJsonRpc from 'bitcoin-json-rpc'; +import { deposit, mineBlocks } from './regtest'; export const sleep = (ms: number) => browser.pause(ms); @@ -646,37 +646,50 @@ export async function getAddressFromQRCode(which: addressType): Promise return address; } -export async function mineBlocks(rpc: BitcoinJsonRpc, blocks: number = 1) { - for (let i = 0; i < blocks; i++) { - await rpc.generateToAddress(1, await rpc.getNewAddress()); +/** + * Funds the wallet on regtest. + * Gets the receive address from the app, deposits sats, and optionally mines blocks. + * Uses local Bitcoin RPC or Blocktank API based on BACKEND env var. + */ +export async function fundOnchainWallet({ + sats, + blocksToMine = 1, +}: { + sats?: number; + blocksToMine?: number; +} = {}) { + const address = await getReceiveAddress(); + await swipeFullScreen('down'); + await deposit(address, sats); + if (blocksToMine > 0) { + await mineBlocks(blocksToMine); } } -export async function receiveOnchainFunds( - rpc: BitcoinJsonRpc, - { - sats = 100_000, - blocksToMine = 1, - expectHighBalanceWarning = false, - }: { - sats?: number; - blocksToMine?: number; - expectHighBalanceWarning?: boolean; - } = {} -) { - // convert sats → btc string - const btc = (sats / 100_000_000).toString(); +/** + * Receives onchain funds and verifies the balance. + * Uses local Bitcoin RPC or Blocktank API based on BACKEND env var. + */ +export async function receiveOnchainFunds({ + sats = 100_000, + blocksToMine = 1, + expectHighBalanceWarning = false, +}: { + sats?: number; + blocksToMine?: number; + expectHighBalanceWarning?: boolean; +} = {}) { // format sats with spaces every 3 digits const formattedSats = sats.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); // receive some first const address = await getReceiveAddress(); await swipeFullScreen('down'); - await rpc.sendToAddress(address, btc); + await deposit(address, sats); await acknowledgeReceivedPayment(); - await mineBlocks(rpc, blocksToMine); + await mineBlocks(blocksToMine); const moneyText = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); await expect(moneyText).toHaveText(formattedSats); diff --git a/test/helpers/constants.ts b/test/helpers/constants.ts index 600d1e4..a79bbb3 100644 --- a/test/helpers/constants.ts +++ b/test/helpers/constants.ts @@ -18,10 +18,15 @@ export function getAppPath(): string { throw new Error(`App path not defined in capabilities (tried ${possibleKeys.join(', ')})`); } -export const bitcoinURL = 'http://polaruser:polarpass@127.0.0.1:43782'; +export const bitcoinURL = + process.env.BITCOIN_RPC_URL ?? 'http://polaruser:polarpass@127.0.0.1:43782'; export const electrumHost = '127.0.0.1'; export const electrumPort = 60001; +// Blocktank API for regtest operations (deposit, mine blocks, pay invoices) +export const blocktankURL = + process.env.BLOCKTANK_URL ?? 'https://api.stag0.blocktank.to/blocktank/api/v2'; + export type LndConfig = { server: string; tls: string; diff --git a/test/helpers/regtest.ts b/test/helpers/regtest.ts new file mode 100644 index 0000000..79eae47 --- /dev/null +++ b/test/helpers/regtest.ts @@ -0,0 +1,181 @@ +/** + * Regtest helpers that abstract the backend (local Bitcoin RPC vs Blocktank API). + * + * Set BACKEND=local to use local docker stack (Bitcoin RPC on localhost). + * Set BACKEND=regtest to use Blocktank API (company regtest over the internet). + * + * Default is 'local' for backwards compatibility with existing tests. + */ + +import BitcoinJsonRpc from 'bitcoin-json-rpc'; +import { bitcoinURL, blocktankURL } from './constants'; + +export type Backend = 'local' | 'regtest'; + +export function getBackend(): Backend { + const backend = process.env.BACKEND ?? 'local'; + if (backend !== 'local' && backend !== 'regtest') { + throw new Error(`Invalid BACKEND: ${backend}. Expected 'local' or 'regtest'.`); + } + return backend; +} + +// Local backend (Bitcoin RPC) + +let _rpc: BitcoinJsonRpc | null = null; + +function getRpc(): BitcoinJsonRpc { + if (!_rpc) { + _rpc = new BitcoinJsonRpc(bitcoinURL); + } + return _rpc; +} + +async function localDeposit(address: string, amountSat?: number): Promise { + const rpc = getRpc(); + const btc = amountSat ? (amountSat / 100_000_000).toString() : '0.001'; // default 100k sats + console.info(`→ [local] Sending ${btc} BTC to ${address}`); + const txid = await rpc.sendToAddress(address, btc); + console.info(`→ [local] txid: ${txid}`); + return txid; +} + +async function localMineBlocks(count: number): Promise { + const rpc = getRpc(); + console.info(`→ [local] Mining ${count} block(s)...`); + for (let i = 0; i < count; i++) { + await rpc.generateToAddress(1, await rpc.getNewAddress()); + } + console.info(`→ [local] Mined ${count} block(s)`); +} + +// Blocktank backend (regtest API over HTTPS) + +async function blocktankDeposit(address: string, amountSat?: number): Promise { + const url = `${blocktankURL}/regtest/chain/deposit`; + const body: { address: string; amountSat?: number } = { address }; + if (amountSat !== undefined) { + body.amountSat = amountSat; + } + + console.info(`→ [blocktank] Deposit to ${address}${amountSat ? ` (${amountSat} sats)` : ''}`); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Blocktank deposit failed: ${response.status} - ${errorText}`); + } + + const txid = await response.text(); + console.info(`→ [blocktank] txid: ${txid}`); + return txid; +} + +async function blocktankMineBlocks(count: number): Promise { + const url = `${blocktankURL}/regtest/chain/mine`; + + console.info(`→ [blocktank] Mining ${count} block(s)...`); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ count }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Blocktank mine failed: ${response.status} - ${errorText}`); + } + + console.info(`→ [blocktank] Mined ${count} block(s)`); +} + +async function blocktankPayInvoice(invoice: string, amountSat?: number): Promise { + const url = `${blocktankURL}/regtest/channel/pay`; + const body: { invoice: string; amountSat?: number } = { invoice }; + if (amountSat !== undefined) { + body.amountSat = amountSat; + } + + console.info(`→ [blocktank] Paying invoice...`); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Blocktank pay invoice failed: ${response.status} - ${errorText}`); + } + + const paymentId = await response.text(); + console.info(`→ [blocktank] Payment ID: ${paymentId}`); + return paymentId; +} + +// Unified interface + +/** + * Returns the Bitcoin RPC client for direct operations. + * Only works with BACKEND=local. Throws if using regtest backend. + * Useful for test utilities that need direct RPC access (e.g., getting addresses to send TO). + */ +export function getBitcoinRpc(): BitcoinJsonRpc { + const backend = getBackend(); + if (backend !== 'local') { + throw new Error('getBitcoinRpc() only works with BACKEND=local'); + } + return getRpc(); +} + +/** + * Deposits satoshis to an address on regtest. + * Uses local Bitcoin RPC or Blocktank API based on BACKEND env var. + * + * @param address - The Bitcoin address to fund + * @param amountSat - Amount in satoshis (optional) + * @returns The transaction ID + */ +export async function deposit(address: string, amountSat?: number): Promise { + const backend = getBackend(); + if (backend === 'local') { + return localDeposit(address, amountSat); + } else { + return blocktankDeposit(address, amountSat); + } +} + +/** + * Mines blocks on regtest. + * Uses local Bitcoin RPC or Blocktank API based on BACKEND env var. + * + * @param count - Number of blocks to mine (default: 1) + */ +export async function mineBlocks(count: number = 1): Promise { + const backend = getBackend(); + if (backend === 'local') { + return localMineBlocks(count); + } else { + return blocktankMineBlocks(count); + } +} + +/** + * Pays a Lightning invoice on regtest. + * Only available with Blocktank backend (regtest). + * + * @param invoice - The BOLT11 invoice to pay + * @param amountSat - Amount in satoshis (optional, for amount-less invoices) + * @returns The payment ID + */ +export async function payInvoice(invoice: string, amountSat?: number): Promise { + const backend = getBackend(); + if (backend === 'local') { + throw new Error('payInvoice is only available with BACKEND=regtest (Blocktank API)'); + } + return blocktankPayInvoice(invoice, amountSat); +} diff --git a/test/specs/backup.e2e.ts b/test/specs/backup.e2e.ts index 3ea60d6..e2b3886 100644 --- a/test/specs/backup.e2e.ts +++ b/test/specs/backup.e2e.ts @@ -1,5 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; -import { bitcoinURL } from '../helpers/constants'; import initElectrum from '../helpers/electrum'; import { reinstallApp } from '../helpers/setup'; import { @@ -19,10 +17,11 @@ import { waitForBackup, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc } from '../helpers/regtest'; describe('@backup - Backup', () => { let electrum: Awaited> | undefined; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { // ensure we have at least 10 BTC on regtest @@ -57,7 +56,7 @@ describe('@backup - Backup', () => { // - check if everything was restored // - receive some money // - await receiveOnchainFunds(rpc, { sats: 100_000_000, expectHighBalanceWarning: true }); + await receiveOnchainFunds({ sats: 100_000_000, expectHighBalanceWarning: true }); // - set tag // const tag = 'testtag'; diff --git a/test/specs/boost.e2e.ts b/test/specs/boost.e2e.ts index 69f7253..d797510 100644 --- a/test/specs/boost.e2e.ts +++ b/test/specs/boost.e2e.ts @@ -1,5 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; - import { sleep, completeOnboarding, @@ -12,7 +10,6 @@ import { expectTextWithin, elementByIdWithin, getTextUnder, - mineBlocks, doNavigationClose, getSeed, waitForBackup, @@ -20,14 +17,14 @@ import { enterAddress, waitForToast, } from '../helpers/actions'; -import { bitcoinURL } from '../helpers/constants'; import initElectrum from '../helpers/electrum'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@boost - Boost', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { let balance = await rpc.getBalance(); @@ -52,7 +49,7 @@ describe('@boost - Boost', () => { ciIt('@boost_1 - Can do CPFP', async () => { // fund the wallet (100 000), don't mine blocks so tx is unconfirmed - await receiveOnchainFunds(rpc, { sats: 100_000, blocksToMine: 0 }); + await receiveOnchainFunds({ sats: 100_000, blocksToMine: 0 }); // check Activity await swipeFullScreen('up'); @@ -125,7 +122,7 @@ describe('@boost - Boost', () => { await elementById('StatusBoosting').waitForDisplayed(); // mine new block - await mineBlocks(rpc, 1); + await mineBlocks(1); await doNavigationClose(); await sleep(500); @@ -142,7 +139,7 @@ describe('@boost - Boost', () => { ciIt('@boost_2 - Can do RBF', async () => { // fund the wallet (100 000) - await receiveOnchainFunds(rpc); + await receiveOnchainFunds(); // Send 10 000 const coreAddress = await rpc.getNewAddress(); @@ -231,7 +228,7 @@ describe('@boost - Boost', () => { await doNavigationClose(); // mine new block - await mineBlocks(rpc, 1); + await mineBlocks(1); await doNavigationClose(); await sleep(500); diff --git a/test/specs/lightning.e2e.ts b/test/specs/lightning.e2e.ts index b6e4db7..c9b88a1 100644 --- a/test/specs/lightning.e2e.ts +++ b/test/specs/lightning.e2e.ts @@ -1,4 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; import initElectrum from '../helpers/electrum'; import { completeOnboarding, @@ -19,7 +18,6 @@ import { getAddressFromQRCode, getSeed, restoreWallet, - mineBlocks, elementByText, dismissQuickPayIntro, doNavigationClose, @@ -29,7 +27,7 @@ import { waitForToast, } from '../helpers/actions'; import { reinstallApp } from '../helpers/setup'; -import { bitcoinURL, lndConfig } from '../helpers/constants'; +import { lndConfig } from '../helpers/constants'; import { connectToLND, getLDKNodeID, @@ -40,10 +38,11 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@lightning - Lightning', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { let balance = await rpc.getBalance(); @@ -77,7 +76,7 @@ describe('@lightning - Lightning', () => { // - check balances, tx history and notes // - close channel - await receiveOnchainFunds(rpc, { sats: 1000 }); + await receiveOnchainFunds({ sats: 1000 }); // send funds to LND node and open a channel const { lnd, lndNodeID } = await setupLND(rpc, lndConfig); @@ -293,7 +292,7 @@ describe('@lightning - Lightning', () => { await elementByText('Transfer Initiated').waitForDisplayed(); await elementByText('Transfer Initiated').waitForDisplayed({ reverse: true }); - await mineBlocks(rpc, 6); + await mineBlocks(6); await electrum?.waitForSync(); await elementById('Channel').waitForDisplayed({ reverse: true }); if (driver.isAndroid) { diff --git a/test/specs/lnurl.e2e.ts b/test/specs/lnurl.e2e.ts index faf7591..261a384 100644 --- a/test/specs/lnurl.e2e.ts +++ b/test/specs/lnurl.e2e.ts @@ -1,8 +1,7 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; import LNURL from 'lnurl'; import initElectrum from '../helpers/electrum'; -import { bitcoinURL, lndConfig } from '../helpers/constants'; +import { lndConfig } from '../helpers/constants'; import { sleep, tap, @@ -34,6 +33,7 @@ import { waitForActiveChannel, setupLND, } from '../helpers/lnd'; +import { getBitcoinRpc } from '../helpers/regtest'; function waitForEvent(lnurlServer: any, name: string): Promise { let timer: NodeJS.Timeout | undefined; @@ -57,7 +57,7 @@ function waitForEvent(lnurlServer: any, name: string): Promise { describe('@lnurl - LNURL', () => { let electrum: Awaited> | undefined; let lnurlServer: any; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { // Ensure we have at least 10 BTC on regtest @@ -105,7 +105,7 @@ describe('@lnurl - LNURL', () => { ciIt( '@lnurl_1 - Can process lnurl-channel, lnurl-pay, lnurl-withdraw, and lnurl-auth', async () => { - await receiveOnchainFunds(rpc, { sats: 1000 }); + await receiveOnchainFunds({ sats: 1000 }); // Get LDK node id from the UI const ldkNodeID = await getLDKNodeID(); diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 93c3b52..3d73679 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -26,13 +26,17 @@ const MIGRATION_MNEMONIC = process.env.MIGRATION_MNEMONIC ?? 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; -describe('@migration - Legacy RN migration', () => { +const totalBalance = '141 321'; +const savingBalance = '91 766'; +const spendingBalance = '49 555'; + +describe('@migration - Migration from legacy RN app to native app', () => { ciIt('@migration_1 - Remove legacy RN app and install native app', async () => { await installLegacyRnApp(); await restoreLegacyRnWallet(MIGRATION_MNEMONIC); // Reinstall native app - console.info(`→ Reinstalling app from: ${getNativeAppPath()}`); + console.info(`→ Remove and install app from: ${getNativeAppPath()}`); await driver.removeApp(getAppId()); resetBootedIOSKeychain(); await driver.installApp(getNativeAppPath()); @@ -84,15 +88,11 @@ async function restoreLegacyRnWallet(seed: string) { await getStarted.waitForDisplayed({ timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); - await expectText(totalBalance); - await expectText(savingBalance); - await expectText(spendingBalance); + await expectText(totalBalance, { strategy: 'contains' }); + await expectText(savingBalance, { strategy: 'contains' }); + await expectText(spendingBalance, { strategy: 'contains' }); } -const totalBalance = '141 321'; -const savingBalance = '91 766'; -const spendingBalance = '49 555'; - async function verifyMigration() { console.info('→ Verifying migrated wallet balances...'); const totalBalanceEl = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); diff --git a/test/specs/onchain.e2e.ts b/test/specs/onchain.e2e.ts index 787d701..8858c2e 100644 --- a/test/specs/onchain.e2e.ts +++ b/test/specs/onchain.e2e.ts @@ -1,5 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; -import { bitcoinURL } from '../helpers/constants'; import initElectrum from '../helpers/electrum'; import { reinstallApp } from '../helpers/setup'; import { @@ -12,7 +10,6 @@ import { expectTextWithin, doNavigationClose, getReceiveAddress, - mineBlocks, multiTap, sleep, swipeFullScreen, @@ -27,10 +24,11 @@ import { acknowledgeReceivedPayment, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@onchain - Onchain', () => { let electrum: Awaited> | undefined; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { // ensure we have at least 10 BTC on regtest @@ -57,7 +55,7 @@ describe('@onchain - Onchain', () => { ciIt('@onchain_1 - Receive and send some out', async () => { // receive some first - await receiveOnchainFunds(rpc, { sats: 100_000_000, expectHighBalanceWarning: true }); + await receiveOnchainFunds({ sats: 100_000_000, expectHighBalanceWarning: true }); // then send out 10 000 const coreAddress = await rpc.getNewAddress(); @@ -72,7 +70,7 @@ describe('@onchain - Onchain', () => { await elementById('SendSuccess').waitForDisplayed(); await tap('Close'); - await mineBlocks(rpc, 1); + await mineBlocks(1); await electrum?.waitForSync(); const moneyTextAfter = (await elementsById('MoneyText'))[1]; @@ -127,7 +125,7 @@ describe('@onchain - Onchain', () => { await rpc.sendToAddress(address, '1'); await acknowledgeReceivedPayment(); - await mineBlocks(rpc, 1); + await mineBlocks(1); await electrum?.waitForSync(); await sleep(1000); // wait for the app to settle @@ -166,7 +164,7 @@ describe('@onchain - Onchain', () => { await elementById('SendSuccess').waitForDisplayed(); await tap('Close'); - await mineBlocks(rpc, 1); + await mineBlocks(1); const totalBalance = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); await expect(totalBalance).toHaveText('0'); @@ -258,7 +256,7 @@ describe('@onchain - Onchain', () => { ciIt('@onchain_3 - Avoids creating a dust output and instead adds it to the fee', async () => { // receive some first - await receiveOnchainFunds(rpc, { sats: 100_000_000, expectHighBalanceWarning: true }); + await receiveOnchainFunds({ sats: 100_000_000, expectHighBalanceWarning: true }); // enable warning for sending over 100$ to test multiple warning dialogs await tap('HeaderMenu'); @@ -294,7 +292,7 @@ describe('@onchain - Onchain', () => { await elementById('SendSuccess').waitForDisplayed(); await tap('Close'); - await mineBlocks(rpc, 1); + await mineBlocks(1); await electrum?.waitForSync(); const totalBalanceAfter = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); diff --git a/test/specs/security.e2e.ts b/test/specs/security.e2e.ts index db40dfb..0b2ca05 100644 --- a/test/specs/security.e2e.ts +++ b/test/specs/security.e2e.ts @@ -1,5 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; - import { sleep, completeOnboarding, @@ -13,14 +11,14 @@ import { expectText, doNavigationClose, } from '../helpers/actions'; -import { bitcoinURL } from '../helpers/constants'; import initElectrum from '../helpers/electrum'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc } from '../helpers/regtest'; describe('@security - Security And Privacy', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { let balance = await rpc.getBalance(); @@ -80,7 +78,7 @@ describe('@security - Security And Privacy', () => { await elementById('TotalBalance').waitForDisplayed(); // receive - await receiveOnchainFunds(rpc); + await receiveOnchainFunds(); // send, using PIN const coreAddress = await rpc.getNewAddress(); diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index e5efefc..be129ea 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -1,4 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; import { encode } from 'bip21'; import initElectrum from '../helpers/electrum'; @@ -17,7 +16,6 @@ import { swipeFullScreen, multiTap, typeAddressAndVerifyContinue, - mineBlocks, dismissQuickPayIntro, doNavigationClose, waitForToast, @@ -25,7 +23,7 @@ import { dismissBackgroundPaymentsTimedSheet, acknowledgeReceivedPayment, } from '../helpers/actions'; -import { bitcoinURL, lndConfig } from '../helpers/constants'; +import { lndConfig } from '../helpers/constants'; import { reinstallApp } from '../helpers/setup'; import { confirmInputOnKeyboard, tap, typeText } from '../helpers/actions'; import { @@ -38,10 +36,11 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; +import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@send - Send', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { let balance = await rpc.getBalance(); @@ -96,7 +95,7 @@ describe('@send - Send', () => { // Receive funds and check validation w/ balance await swipeFullScreen('down'); - await receiveOnchainFunds(rpc); + await receiveOnchainFunds(); await tap('Send'); await sleep(500); @@ -143,7 +142,7 @@ describe('@send - Send', () => { // - quickpay to lightning invoice // - quickpay to unified invoice - await receiveOnchainFunds(rpc); + await receiveOnchainFunds(); // send funds to LND node and open a channel const { lnd, lndNodeID } = await setupLND(rpc, lndConfig); @@ -473,7 +472,7 @@ describe('@send - Send', () => { // TEMP: receive more funds to be able to pay 10k invoice console.info('Receiving lightning funds...'); - await mineBlocks(rpc, 1); + await mineBlocks(1); await electrum?.waitForSync(); const receive2 = await getReceiveAddress('lightning'); await swipeFullScreen('down'); diff --git a/test/specs/transfer.e2e.ts b/test/specs/transfer.e2e.ts index e04d0d8..e3e4dc4 100644 --- a/test/specs/transfer.e2e.ts +++ b/test/specs/transfer.e2e.ts @@ -1,5 +1,3 @@ -import BitcoinJsonRpc from 'bitcoin-json-rpc'; - import initElectrum from '../helpers/electrum'; import { completeOnboarding, @@ -14,7 +12,6 @@ import { dragOnElement, expectTextWithin, swipeFullScreen, - mineBlocks, elementByIdWithin, enterAddress, dismissQuickPayIntro, @@ -33,14 +30,15 @@ import { waitForActiveChannel, waitForPeerConnection, } from '../helpers/lnd'; -import { bitcoinURL, lndConfig } from '../helpers/constants'; +import { lndConfig } from '../helpers/constants'; +import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; describe('@transfer - Transfer', () => { let electrum: { waitForSync: () => any; stop: () => void }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + const rpc = getBitcoinRpc(); before(async () => { let balance = await rpc.getBalance(); @@ -77,7 +75,7 @@ describe('@transfer - Transfer', () => { ciIt( '@transfer_1 - Can buy a channel from Blocktank with default and custom receive capacity', async () => { - await receiveOnchainFunds(rpc, { sats: 1000_000, expectHighBalanceWarning: true }); + await receiveOnchainFunds({ sats: 1000_000, expectHighBalanceWarning: true }); // switch to EUR await tap('HeaderMenu'); @@ -302,7 +300,7 @@ describe('@transfer - Transfer', () => { ); ciIt('@transfer_2 - Can open a channel to external node', async () => { - await receiveOnchainFunds(rpc, { sats: 100_000 }); + await receiveOnchainFunds({ sats: 100_000 }); // send funds to LND node and open a channel const { lnd, lndNodeID } = await setupLND(rpc, lndConfig); @@ -362,7 +360,7 @@ describe('@transfer - Transfer', () => { await expectTextWithin('ActivityShort-1', 'Received'); await swipeFullScreen('down'); - await mineBlocks(rpc, 6); + await mineBlocks(6); await electrum?.waitForSync(); await waitForToast('SpendingBalanceReadyToast'); await sleep(1000); From 37cd7afaa6725c4e8dc192c7331b8e6f223d6ae0 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 23:14:55 +0100 Subject: [PATCH 08/39] ensureLocalFunds and rpc initialization --- test/helpers/regtest.ts | 70 ++++++++++++++++++++++++++++++++++++- test/specs/backup.e2e.ts | 13 ++----- test/specs/boost.e2e.ts | 14 ++------ test/specs/lightning.e2e.ts | 15 +++----- test/specs/lnurl.e2e.ts | 17 ++++----- test/specs/onchain.e2e.ts | 21 ++++------- test/specs/security.e2e.ts | 14 ++------ test/specs/send.e2e.ts | 17 ++++----- test/specs/transfer.e2e.ts | 15 +++----- 9 files changed, 105 insertions(+), 91 deletions(-) diff --git a/test/helpers/regtest.ts b/test/helpers/regtest.ts index 79eae47..f9cb11e 100644 --- a/test/helpers/regtest.ts +++ b/test/helpers/regtest.ts @@ -13,7 +13,7 @@ import { bitcoinURL, blocktankURL } from './constants'; export type Backend = 'local' | 'regtest'; export function getBackend(): Backend { - const backend = process.env.BACKEND ?? 'local'; + const backend = process.env.BACKEND || 'local'; // Use || to handle empty string if (backend !== 'local' && backend !== 'regtest') { throw new Error(`Invalid BACKEND: ${backend}. Expected 'local' or 'regtest'.`); } @@ -132,6 +132,74 @@ export function getBitcoinRpc(): BitcoinJsonRpc { return getRpc(); } +/** + * Ensures the local bitcoind has enough funds for testing. + * Only runs when BACKEND=local. Skips silently when BACKEND=regtest + * (Blocktank handles funding via its API). + * + * Call this in test `before` hooks instead of directly using RPC. + */ +export async function ensureLocalFunds(minBtc: number = 10): Promise { + const backend = getBackend(); + if (backend !== 'local') { + console.info(`→ [${backend}] Skipping local bitcoind funding (using Blocktank API)`); + return; + } + + const rpc = getRpc(); + let balance = await rpc.getBalance(); + const address = await rpc.getNewAddress(); + + while (balance < minBtc) { + console.info(`→ [local] Mining blocks to fund local bitcoind (balance: ${balance} BTC)...`); + await rpc.generateToAddress(10, address); + balance = await rpc.getBalance(); + } + console.info(`→ [local] Local bitcoind has ${balance} BTC`); +} + +// Known regtest address for send tests (used when BACKEND=regtest) +// This is a standard regtest address that always works +const REGTEST_TEST_ADDRESS = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080'; + +/** + * Returns an external address to send funds TO (for testing send functionality). + * - BACKEND=local: generates a new address from local bitcoind + * - BACKEND=regtest: returns a known regtest test address + */ +export async function getExternalAddress(): Promise { + const backend = getBackend(); + if (backend === 'local') { + const rpc = getRpc(); + return rpc.getNewAddress(); + } + return REGTEST_TEST_ADDRESS; +} + +/** + * Sends funds to an address (for testing receive in the app). + * - BACKEND=local: uses local bitcoind RPC + * - BACKEND=regtest: uses Blocktank deposit API + * + * @param address - The address to send to + * @param amountBtcOrSats - Amount (BTC string for local, sats number for regtest) + */ +export async function sendToAddress(address: string, amountBtcOrSats: string | number): Promise { + const backend = getBackend(); + if (backend === 'local') { + const rpc = getRpc(); + const btc = typeof amountBtcOrSats === 'number' + ? (amountBtcOrSats / 100_000_000).toString() + : amountBtcOrSats; + return rpc.sendToAddress(address, btc); + } else { + const sats = typeof amountBtcOrSats === 'string' + ? Math.round(parseFloat(amountBtcOrSats) * 100_000_000) + : amountBtcOrSats; + return blocktankDeposit(address, sats); + } +} + /** * Deposits satoshis to an address on regtest. * Uses local Bitcoin RPC or Blocktank API based on BACKEND env var. diff --git a/test/specs/backup.e2e.ts b/test/specs/backup.e2e.ts index e2b3886..2d5c9ca 100644 --- a/test/specs/backup.e2e.ts +++ b/test/specs/backup.e2e.ts @@ -17,22 +17,13 @@ import { waitForBackup, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc } from '../helpers/regtest'; +import { ensureLocalFunds } from '../helpers/regtest'; describe('@backup - Backup', () => { let electrum: Awaited> | undefined; - const rpc = getBitcoinRpc(); before(async () => { - // ensure we have at least 10 BTC on regtest - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + await ensureLocalFunds(); electrum = await initElectrum(); }); diff --git a/test/specs/boost.e2e.ts b/test/specs/boost.e2e.ts index d797510..7fb276e 100644 --- a/test/specs/boost.e2e.ts +++ b/test/specs/boost.e2e.ts @@ -20,21 +20,13 @@ import { import initElectrum from '../helpers/electrum'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { ensureLocalFunds, getExternalAddress, mineBlocks } from '../helpers/regtest'; describe('@boost - Boost', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = getBitcoinRpc(); before(async () => { - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + await ensureLocalFunds(); electrum = await initElectrum(); }); @@ -142,7 +134,7 @@ describe('@boost - Boost', () => { await receiveOnchainFunds(); // Send 10 000 - const coreAddress = await rpc.getNewAddress(); + const coreAddress = await getExternalAddress(); await enterAddress(coreAddress); await tap('N1'); await tap('N0'); diff --git a/test/specs/lightning.e2e.ts b/test/specs/lightning.e2e.ts index c9b88a1..3671f15 100644 --- a/test/specs/lightning.e2e.ts +++ b/test/specs/lightning.e2e.ts @@ -38,21 +38,16 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@lightning - Lightning', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = getBitcoinRpc(); + // LND tests only work with BACKEND=local + let rpc: ReturnType; before(async () => { - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + rpc = getBitcoinRpc(); + await ensureLocalFunds(); electrum = await initElectrum(); }); diff --git a/test/specs/lnurl.e2e.ts b/test/specs/lnurl.e2e.ts index 261a384..1ba6586 100644 --- a/test/specs/lnurl.e2e.ts +++ b/test/specs/lnurl.e2e.ts @@ -33,7 +33,7 @@ import { waitForActiveChannel, setupLND, } from '../helpers/lnd'; -import { getBitcoinRpc } from '../helpers/regtest'; +import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; function waitForEvent(lnurlServer: any, name: string): Promise { let timer: NodeJS.Timeout | undefined; @@ -57,17 +57,12 @@ function waitForEvent(lnurlServer: any, name: string): Promise { describe('@lnurl - LNURL', () => { let electrum: Awaited> | undefined; let lnurlServer: any; - const rpc = getBitcoinRpc(); + // LND tests only work with BACKEND=local + let rpc: ReturnType; before(async () => { - // Ensure we have at least 10 BTC on regtest - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + rpc = getBitcoinRpc(); + await ensureLocalFunds(); electrum = await initElectrum(); // Start local LNURL server backed by LND REST @@ -134,7 +129,7 @@ describe('@lnurl - LNURL', () => { await waitForPeerConnection(lnd as any, ldkNodeID); // Confirm channel by mining and syncing - await rpc.generateToAddress(6, await rpc.getNewAddress()); + await mineBlocks(6); await electrum?.waitForSync(); // Wait for channel to be active diff --git a/test/specs/onchain.e2e.ts b/test/specs/onchain.e2e.ts index 8858c2e..2607081 100644 --- a/test/specs/onchain.e2e.ts +++ b/test/specs/onchain.e2e.ts @@ -24,22 +24,13 @@ import { acknowledgeReceivedPayment, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { ensureLocalFunds, getExternalAddress, mineBlocks, sendToAddress } from '../helpers/regtest'; describe('@onchain - Onchain', () => { let electrum: Awaited> | undefined; - const rpc = getBitcoinRpc(); before(async () => { - // ensure we have at least 10 BTC on regtest - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + await ensureLocalFunds(); electrum = await initElectrum(); }); @@ -58,7 +49,7 @@ describe('@onchain - Onchain', () => { await receiveOnchainFunds({ sats: 100_000_000, expectHighBalanceWarning: true }); // then send out 10 000 - const coreAddress = await rpc.getNewAddress(); + const coreAddress = await getExternalAddress(); console.info({ coreAddress }); await enterAddress(coreAddress); await tap('N1'); @@ -122,7 +113,7 @@ describe('@onchain - Onchain', () => { await tap('ShowQrReceive'); await swipeFullScreen('down'); - await rpc.sendToAddress(address, '1'); + await sendToAddress(address, '1'); await acknowledgeReceivedPayment(); await mineBlocks(1); @@ -141,7 +132,7 @@ describe('@onchain - Onchain', () => { } // - can send total balance and tag the tx // - const coreAddress = await rpc.getNewAddress(); + const coreAddress = await getExternalAddress(); await enterAddress(coreAddress); // Amount / NumberPad @@ -265,7 +256,7 @@ describe('@onchain - Onchain', () => { await tap('SendAmountWarning'); await doNavigationClose(); - const coreAddress = await rpc.getNewAddress(); + const coreAddress = await getExternalAddress(); console.info({ coreAddress }); await enterAddress(coreAddress); diff --git a/test/specs/security.e2e.ts b/test/specs/security.e2e.ts index 0b2ca05..e67e0d5 100644 --- a/test/specs/security.e2e.ts +++ b/test/specs/security.e2e.ts @@ -14,21 +14,13 @@ import { import initElectrum from '../helpers/electrum'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc } from '../helpers/regtest'; +import { ensureLocalFunds, getExternalAddress } from '../helpers/regtest'; describe('@security - Security And Privacy', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = getBitcoinRpc(); before(async () => { - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + await ensureLocalFunds(); electrum = await initElectrum(); }); @@ -81,7 +73,7 @@ describe('@security - Security And Privacy', () => { await receiveOnchainFunds(); // send, using PIN - const coreAddress = await rpc.getNewAddress(); + const coreAddress = await getExternalAddress(); await enterAddress(coreAddress); await tap('N1'); await tap('N000'); diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index be129ea..76f76e8 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -36,21 +36,16 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; -import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { ensureLocalFunds, getBitcoinRpc, getExternalAddress, mineBlocks } from '../helpers/regtest'; describe('@send - Send', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = getBitcoinRpc(); + // LND tests only work with BACKEND=local + let rpc: ReturnType; before(async () => { - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + rpc = getBitcoinRpc(); + await ensureLocalFunds(); electrum = await initElectrum(); }); @@ -102,7 +97,7 @@ describe('@send - Send', () => { await tap('RecipientManual'); // check validation for address - const address2 = await rpc.getNewAddress(); + const address2 = await getExternalAddress(); try { await typeAddressAndVerifyContinue({ address: address2 }); } catch { diff --git a/test/specs/transfer.e2e.ts b/test/specs/transfer.e2e.ts index e3e4dc4..42b6081 100644 --- a/test/specs/transfer.e2e.ts +++ b/test/specs/transfer.e2e.ts @@ -31,24 +31,19 @@ import { waitForPeerConnection, } from '../helpers/lnd'; import { lndConfig } from '../helpers/constants'; -import { getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; describe('@transfer - Transfer', () => { let electrum: { waitForSync: () => any; stop: () => void }; - const rpc = getBitcoinRpc(); + // LND tests only work with BACKEND=local + let rpc: ReturnType; before(async () => { - let balance = await rpc.getBalance(); - const address = await rpc.getNewAddress(); - - while (balance < 10) { - await rpc.generateToAddress(10, address); - balance = await rpc.getBalance(); - } - + rpc = getBitcoinRpc(); + await ensureLocalFunds(); electrum = await initElectrum(); }); From fe638587e07ee25dc28e27789c9b8b3aa5ef06ff Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 23:22:16 +0100 Subject: [PATCH 09/39] electrum noop for regtest --- test/helpers/electrum.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/helpers/electrum.ts b/test/helpers/electrum.ts index 7b55806..44ce45b 100644 --- a/test/helpers/electrum.ts +++ b/test/helpers/electrum.ts @@ -3,6 +3,7 @@ import tls from 'tls'; import BitcoinJsonRpc from 'bitcoin-json-rpc'; import * as electrum from 'rn-electrum-client/helpers'; import { bitcoinURL, electrumHost, electrumPort } from './constants'; +import { getBackend } from './regtest'; const peer = { host: electrumHost, @@ -17,11 +18,33 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -// Connect to the Bitcoin Core node and Electrum server to wait for Electrum to sync -const initElectrum = async (): Promise<{ +export type ElectrumClient = { waitForSync: () => Promise; stop: () => Promise; -}> => { +}; + +// No-op electrum client for regtest backend (app connects to remote Electrum directly) +const noopElectrum: ElectrumClient = { + waitForSync: async () => { + // For regtest backend, we just wait a bit for the app to sync with remote Electrum + console.info('→ [regtest] Waiting for app to sync with remote Electrum...'); + await sleep(2000); + }, + stop: async () => { + // Nothing to stop for regtest + }, +}; + +// Connect to the Bitcoin Core node and Electrum server to wait for Electrum to sync +const initElectrum = async (): Promise => { + const backend = getBackend(); + + // For regtest backend, return no-op client (app connects to remote Electrum directly) + if (backend !== 'local') { + console.info(`→ [${backend}] Skipping local Electrum init (using remote Electrum)`); + return noopElectrum; + } + let electrumHeight = 0; try { From 891b768da9f286aed1a86f2ff64d3163caff1230 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 7 Jan 2026 13:26:17 +0100 Subject: [PATCH 10/39] formatting and small updates --- scripts/build-rn-android-apk.sh | 2 +- test/specs/send.e2e.ts | 4 +++- wdio.conf.ts | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/build-rn-android-apk.sh b/scripts/build-rn-android-apk.sh index 6c72ac2..9b15c2e 100755 --- a/scripts/build-rn-android-apk.sh +++ b/scripts/build-rn-android-apk.sh @@ -27,7 +27,7 @@ fi if [[ -z "${ENV_FILE:-}" ]]; then if [[ "$BACKEND" == "regtest" ]]; then - ENV_FILE=".env.test.template" + ENV_FILE=".env.development.template" else ENV_FILE=".env.development" fi diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index f7bbb27..e5efefc 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -356,7 +356,9 @@ describe('@send - Send', () => { await expectTextWithin('ActivitySpending', '7 000'); } else { // https://github.com/synonymdev/bitkit-ios/issues/300 - console.info('Skipping sending to unified invoice w/ expired invoice on iOS due to /bitkit-ios/issues/300'); + console.info( + 'Skipping sending to unified invoice w/ expired invoice on iOS due to /bitkit-ios/issues/300' + ); amtAfterUnified3 = amtAfterUnified2; } diff --git a/wdio.conf.ts b/wdio.conf.ts index 7f5dad1..4aaedd9 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -65,7 +65,6 @@ export const config: WebdriverIO.Config = { 'appium:deviceName': 'Pixel_6', 'appium:platformVersion': '13.0', 'appium:app': path.join(__dirname, 'aut', 'bitkit_e2e.apk'), - // 'appium:app': path.join(__dirname, 'aut', 'bitkit_v1.1.2.apk'), 'appium:autoGrantPermissions': true, } : { From 8d6c6802d3661cbfe3e4a7495505f25c4fe7afd7 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 7 Jan 2026 13:26:51 +0100 Subject: [PATCH 11/39] basic migration scenarios for android --- test/helpers/actions.ts | 8 ++- test/helpers/setup.ts | 6 +- test/specs/migration.e2e.ts | 125 +++++++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index f1eb58b..0142fc4 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -522,14 +522,18 @@ export async function restoreWallet( { passphrase, expectQuickPayTimedSheet = false, - }: { passphrase?: string; expectQuickPayTimedSheet?: boolean } = {} + reinstall = true, + }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; reinstall?: boolean } = {} ) { console.info('→ Restoring wallet with seed:', seed); // Let cloud state flush - carried over from Detox await sleep(5000); // Reinstall app to wipe all data - await reinstallApp(); + if (reinstall) { + console.info('Reinstalling app to reset state...'); + await reinstallApp(); + } // Terms of service await elementById('Continue').waitForDisplayed(); diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 911b7a0..8c86a3b 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -30,9 +30,7 @@ export function getRnAppPath(): string { const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_rn_regtest.apk'); const appPath = process.env.RN_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { - throw new Error( - `RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}` - ); + throw new Error(`RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}`); } return appPath; } @@ -61,7 +59,7 @@ export async function reinstallAppFromPath(appPath: string, appId: string = getA * (Wallet data is stored in iOS Keychain and persists even after app uninstall * unless the whole simulator is reset or keychain is reset specifically) */ -function resetBootedIOSKeychain() { +export function resetBootedIOSKeychain() { if (!driver.isIOS) return; let udid = ''; diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 8d03450..2650aff 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,25 +1,64 @@ -import { elementById, restoreWallet, sleep, tap, typeText, waitForSetupWalletScreenFinish } from '../helpers/actions'; +import { + dismissBackupTimedSheet, + elementById, + elementByIdWithin, + expectText, + expectTextWithin, + handleAndroidAlert, + restoreWallet, + sleep, + swipeFullScreen, + tap, + typeText, + waitForSetupWalletScreenFinish, +} from '../helpers/actions'; import { ciIt } from '../helpers/suite'; -import { getNativeAppPath, getRnAppPath, reinstallAppFromPath } from '../helpers/setup'; +import { + getNativeAppPath, + getRnAppPath, + reinstallAppFromPath, + resetBootedIOSKeychain, +} from '../helpers/setup'; +import { getAppId } from '../helpers/constants'; const MIGRATION_MNEMONIC = process.env.MIGRATION_MNEMONIC ?? 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; describe('@migration - Legacy RN migration', () => { - ciIt('@migration_1 - Can restore legacy RN wallet from mnemonic', async () => { + ciIt('@migration_1 - Remove legacy RN app and install native app', async () => { await installLegacyRnApp(); await restoreLegacyRnWallet(MIGRATION_MNEMONIC); - // Restore into native app - // await installNativeApp(); - await restoreWallet(MIGRATION_MNEMONIC); + // Reinstall native app + console.info(`→ Reinstalling app from: ${getNativeAppPath()}`); + await driver.removeApp(getAppId()); + resetBootedIOSKeychain(); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // restore wallet and verify migration + await restoreWallet(MIGRATION_MNEMONIC, { reinstall: false }); + await verifyMigration(); + }); + + ciIt('@migration_2 - Install native app on top of legacy RN app', async () => { + await installLegacyRnApp(); + await restoreLegacyRnWallet(MIGRATION_MNEMONIC); + + // Install native app + console.info(`→ Installing app from: ${getNativeAppPath()}`); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // verify migration + await handleAndroidAlert(); + await expectText('Migration Complete'); + await dismissBackupTimedSheet(); + await verifyMigration(); }); }); -async function installNativeApp() { - await reinstallAppFromPath(getNativeAppPath()); -} async function installLegacyRnApp() { await reinstallAppFromPath(getRnAppPath()); } @@ -41,7 +80,73 @@ async function restoreLegacyRnWallet(seed: string) { await waitForSetupWalletScreenFinish(); const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed( { timeout: 120000 }); + await getStarted.waitForDisplayed({ timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); + await expectText(totalBalance); + await expectText(savingBalance); + await expectText(spendingBalance); +} + +const totalBalance = '141 321'; +const savingBalance = '91 766'; +const spendingBalance = '49 555'; + +async function verifyMigration() { + console.info('→ Verifying migrated wallet balances...'); + const totalBalanceEl = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); + await expect(totalBalanceEl).toHaveText(totalBalance); + await expectTextWithin('ActivitySpending', spendingBalance); + await expectTextWithin('ActivitySavings', savingBalance); + + console.info('→ Verify transaction details...'); + await swipeFullScreen('up'); + await swipeFullScreen('up'); + await tap('ActivityShowAll'); + + // All transactions + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '+'); + await expectTextWithin('Activity-3', '-'); + await expectTextWithin('Activity-4', '-'); + await expectTextWithin('Activity-5', '+'); + await expectTextWithin('Activity-6', '+'); + + // Sent, 2 transactions + await tap('Tab-sent'); + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '-'); + await expectTextWithin('Activity-3', '-'); + await elementById('Activity-4').waitForDisplayed({ reverse: true }); + + // Received, 2 transactions + await tap('Tab-received'); + await expectTextWithin('Activity-1', '+'); + await expectTextWithin('Activity-2', '+'); + await expectTextWithin('Activity-3', '+'); + await elementById('Activity-4').waitForDisplayed({ reverse: true }); + + // Other, 0 transactions + await tap('Tab-other'); + await elementById('Activity-1').waitForDisplayed(); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + + // filter by receive tag + await tap('Tab-all'); + await tap('TagsPrompt'); + await sleep(500); + await tap('Tag-received '); + await expectTextWithin('Activity-1', '+'); + await expectTextWithin('Activity-2', '+'); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); + await tap('Tag-received -delete'); + + // filter by send tag + await tap('TagsPrompt'); + await sleep(500); + await tap('Tag-sent'); + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '-'); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); + await tap('Tag-sent-delete'); } From 2efc644b784d72d5df6eacac6bef1c8672723270 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 10:19:23 +0100 Subject: [PATCH 12/39] bitkit RN build script --- README.md | 6 +++ scripts/build-rn-ios-sim.sh | 73 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100755 scripts/build-rn-ios-sim.sh diff --git a/README.md b/README.md index 19acfcf..636fcbb 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ If you have `bitkit-e2e-tests`, `bitkit-android`, and `bitkit-ios` checked out i # Legacy RN Android (builds ../bitkit and copies APK to ./aut/bitkit_rn_regtest.apk) ./scripts/build-rn-android-apk.sh +# Legacy RN iOS simulator (builds ../bitkit and copies app to ./aut/bitkit_rn_regtest_ios.app) +./scripts/build-rn-ios-sim.sh + # iOS (builds ../bitkit-ios and copies IPA to ./aut/bitkit_e2e.ipa) ./scripts/build-ios-sim.sh ``` @@ -73,6 +76,9 @@ BACKEND=regtest ./scripts/build-android-apk.sh # Legacy RN Android BACKEND=regtest ./scripts/build-rn-android-apk.sh +# Legacy RN iOS simulator +BACKEND=regtest ./scripts/build-rn-ios-sim.sh + # iOS BACKEND=local ./scripts/build-ios-sim.sh BACKEND=regtest ./scripts/build-ios-sim.sh diff --git a/scripts/build-rn-ios-sim.sh b/scripts/build-rn-ios-sim.sh new file mode 100755 index 0000000..ee3960b --- /dev/null +++ b/scripts/build-rn-ios-sim.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Build the legacy Bitkit RN iOS simulator app from ../bitkit and copy into aut/ +# +# Inputs/roots: +# - E2E root: this repo (bitkit-e2e-tests) +# - RN root: ../bitkit (resolved relative to this script) +# +# Output: +# - Copies .app -> aut/bitkit_rn_regtest_ios.app +# +# Usage: +# ./scripts/build-rn-ios-sim.sh [debug|release] +# BACKEND=regtest ./scripts/build-rn-ios-sim.sh +# ENV_FILE=.env.test.template ./scripts/build-rn-ios-sim.sh +set -euo pipefail + +E2E_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +RN_ROOT="$(cd "$E2E_ROOT/../bitkit" && pwd)" + +BUILD_TYPE="${1:-debug}" +BACKEND="${BACKEND:-regtest}" + +if [[ "$BUILD_TYPE" != "debug" && "$BUILD_TYPE" != "release" ]]; then + echo "ERROR: Unsupported build type: $BUILD_TYPE (expected debug|release)" >&2 + exit 1 +fi + +if [[ -z "${ENV_FILE:-}" ]]; then + if [[ "$BACKEND" == "regtest" ]]; then + ENV_FILE=".env.development.template" + else + ENV_FILE=".env.development" + fi +fi + +if [[ ! -f "$RN_ROOT/$ENV_FILE" ]]; then + echo "ERROR: Env file not found: $RN_ROOT/$ENV_FILE" >&2 + exit 1 +fi + +echo "Building RN iOS simulator app (BACKEND=$BACKEND, ENV_FILE=$ENV_FILE, BUILD_TYPE=$BUILD_TYPE)..." + +pushd "$RN_ROOT" >/dev/null +if [[ -f .env ]]; then + cp .env .env.bak +fi +cp "$ENV_FILE" .env +E2E_TESTS=true yarn "e2e:build:ios-$BUILD_TYPE" +if [[ -f .env.bak ]]; then + mv .env.bak .env +else + rm -f .env +fi +popd >/dev/null + +if [[ "$BUILD_TYPE" == "debug" ]]; then + IOS_CONFIG="Debug" +else + IOS_CONFIG="Release" +fi + +APP_PATH="$RN_ROOT/ios/build/Build/Products/${IOS_CONFIG}-iphonesimulator/bitkit.app" +if [[ ! -d "$APP_PATH" ]]; then + echo "ERROR: iOS .app not found at: $APP_PATH" >&2 + exit 1 +fi + +OUT="$E2E_ROOT/aut" +mkdir -p "$OUT" +OUT_APP="$OUT/bitkit_rn_${BACKEND}_ios.app" +rm -rf "$OUT_APP" +cp -R "$APP_PATH" "$OUT_APP" +echo "RN iOS simulator app copied to: $OUT_APP" From 226036e44434704e5092088623a6ee8eb84fe9d5 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 10:19:58 +0100 Subject: [PATCH 13/39] adjustments for iOS --- test/helpers/actions.ts | 6 +++--- test/helpers/setup.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 0142fc4..7e44467 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -88,11 +88,11 @@ export function elementByText( } else { if (strategy === 'exact') { return $( - `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton") AND (label == "${text}" OR value == "${text}")` + `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton" OR type == "XCUIElementTypeOther") AND (label == "${text}" OR value == "${text}")` ); } return $( - `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton") AND label CONTAINS "${text}"` + `-ios predicate string:(type == "XCUIElementTypeStaticText" OR type == "XCUIElementTypeButton" OR type == "XCUIElementTypeOther") AND label CONTAINS "${text}"` ); } } @@ -567,7 +567,7 @@ export async function restoreWallet( // Wait for Get Started const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed(); + await getStarted.waitForDisplayed( { timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); await handleAndroidAlert(); diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 8c86a3b..488f0d0 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -27,7 +27,8 @@ export async function reinstallApp() { } export function getRnAppPath(): string { - const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_rn_regtest.apk'); + const appFileName = driver.isIOS ? 'bitkit_rn_regtest_ios.app' : 'bitkit_rn_regtest.apk'; + const fallback = path.join(__dirname, '..', '..', 'aut', appFileName); const appPath = process.env.RN_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { throw new Error(`RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}`); @@ -36,7 +37,8 @@ export function getRnAppPath(): string { } export function getNativeAppPath(): string { - const fallback = path.join(__dirname, '..', '..', 'aut', 'bitkit_e2e.apk'); + const appFileName = driver.isIOS ? 'bitkit.app' : 'bitkit_e2e.apk'; + const fallback = path.join(__dirname, '..', '..', 'aut', appFileName); const appPath = process.env.NATIVE_APK_PATH ?? fallback; if (!fs.existsSync(appPath)) { throw new Error( From 491bbe8c097cba06c8a0c4efc5ef4e78cb9b4623 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 15:02:35 +0100 Subject: [PATCH 14/39] formatting --- test/helpers/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 7e44467..c1ab4ca 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -567,7 +567,7 @@ export async function restoreWallet( // Wait for Get Started const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed( { timeout: 120000 }); + await getStarted.waitForDisplayed({ timeout: 120000 }); await tap('GetStartedButton'); await sleep(1000); await handleAndroidAlert(); From 743a34fcd5069c67ee950301181232b86f4bc710 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Thu, 8 Jan 2026 18:02:10 +0100 Subject: [PATCH 15/39] adjust verify tx details --- test/specs/migration.e2e.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 2650aff..93c3b52 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,5 +1,6 @@ import { dismissBackupTimedSheet, + doNavigationClose, elementById, elementByIdWithin, expectText, @@ -112,21 +113,20 @@ async function verifyMigration() { await expectTextWithin('Activity-5', '+'); await expectTextWithin('Activity-6', '+'); - // Sent, 2 transactions + // Sent await tap('Tab-sent'); await expectTextWithin('Activity-1', '-'); await expectTextWithin('Activity-2', '-'); - await expectTextWithin('Activity-3', '-'); - await elementById('Activity-4').waitForDisplayed({ reverse: true }); + await elementById('Activity-3').waitForDisplayed({ reverse: true }); - // Received, 2 transactions + // Received await tap('Tab-received'); await expectTextWithin('Activity-1', '+'); await expectTextWithin('Activity-2', '+'); await expectTextWithin('Activity-3', '+'); await elementById('Activity-4').waitForDisplayed({ reverse: true }); - // Other, 0 transactions + // Other await tap('Tab-other'); await elementById('Activity-1').waitForDisplayed(); await elementById('Activity-2').waitForDisplayed({ reverse: true }); @@ -149,4 +149,6 @@ async function verifyMigration() { await expectTextWithin('Activity-2', '-'); await elementById('Activity-3').waitForDisplayed({ reverse: true }); await tap('Tag-sent-delete'); + + await doNavigationClose(); } From 7f4518c161197a392424bce643a28d2833f9f5da Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 9 Jan 2026 09:39:26 +0100 Subject: [PATCH 16/39] stability --- test/specs/send.e2e.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index 76f76e8..ce2a1f8 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -392,6 +392,7 @@ describe('@send - Send', () => { await sleep(1000); await enterAddress(unified5, { acceptCameraPermission: false }); // max amount (lightning) + await sleep(500); await tap('AvailableAmount'); await tap('ContinueAmount'); await expectText('4 998', { strategy: 'contains' }); @@ -400,6 +401,7 @@ describe('@send - Send', () => { await tap('NavigationBack'); // max amount (onchain) await tap('AssetButton-switch'); + await sleep(500); await tap('AvailableAmount'); if (driver.isIOS) { // iOS runs an autopilot coin selection step on Continue; when the amount is the true "max" From e9d5fc20599c18bb2dea55c3c3ffe8faf925fe6a Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 9 Jan 2026 13:55:37 +0100 Subject: [PATCH 17/39] basic migration android --- test/helpers/actions.ts | 7 +- test/specs/migration.e2e.ts | 391 ++++++++++++++++++++++++++++-------- 2 files changed, 312 insertions(+), 86 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index cdc2f68..2a991e4 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -522,8 +522,9 @@ export async function restoreWallet( { passphrase, expectQuickPayTimedSheet = false, + expectBackupSheet = false, reinstall = true, - }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; reinstall?: boolean } = {} + }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; expectBackupSheet?: boolean; reinstall?: boolean } = {} ) { console.info('→ Restoring wallet with seed:', seed); // Let cloud state flush - carried over from Detox @@ -572,6 +573,10 @@ export async function restoreWallet( await sleep(1000); await handleAndroidAlert(); + if (expectBackupSheet) { + await dismissBackupTimedSheet(); + } + if (expectQuickPayTimedSheet) { await dismissQuickPayIntro(); } diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 3d73679..c78a026 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,10 +1,13 @@ import { dismissBackupTimedSheet, doNavigationClose, + dragOnElement, elementById, elementByIdWithin, + enterAddress, expectText, - expectTextWithin, + getAccessibleText, + getReceiveAddress, handleAndroidAlert, restoreWallet, sleep, @@ -21,134 +24,352 @@ import { resetBootedIOSKeychain, } from '../helpers/setup'; import { getAppId } from '../helpers/constants'; +import initElectrum, { ElectrumClient } from '../helpers/electrum'; +import { deposit, ensureLocalFunds, getExternalAddress, mineBlocks } from '../helpers/regtest'; -const MIGRATION_MNEMONIC = - process.env.MIGRATION_MNEMONIC ?? - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; +// Module-level electrum client (set in before hook) +let electrumClient: ElectrumClient; -const totalBalance = '141 321'; -const savingBalance = '91 766'; -const spendingBalance = '49 555'; +// ============================================================================ +// MIGRATION TEST CONFIGURATION +// ============================================================================ + +// Tags used for testing migration +const TAG_RECEIVED = 'received'; +const TAG_SENT = 'sent'; + +// Amounts for testing +const INITIAL_FUND_SATS = 500_000; // 500k sats initial funding +const ONCHAIN_SEND_SATS = 50_000; // 50k sats for on-chain send test +const TRANSFER_TO_SPENDING_SATS = 100_000; // 100k for creating a channel + +// ============================================================================ +// TEST SUITE +// ============================================================================ describe('@migration - Migration from legacy RN app to native app', () => { - ciIt('@migration_1 - Remove legacy RN app and install native app', async () => { - await installLegacyRnApp(); - await restoreLegacyRnWallet(MIGRATION_MNEMONIC); + let mnemonic: string = ''; + + before(async () => { + await ensureLocalFunds(); + electrumClient = await initElectrum(); + }); + + after(async () => { + await electrumClient?.stop(); + }); + + // -------------------------------------------------------------------------- + // Migration Scenario 1: Uninstall RN, install Native, restore mnemonic + // -------------------------------------------------------------------------- + ciIt('@migration_1 - Uninstall RN app and install native app', async () => { + // Setup wallet in RN app + await setupLegacyWallet(); + + // Get mnemonic before uninstalling + mnemonic = await getRnMnemonic(); + console.info(`→ Got mnemonic: ${mnemonic.substring(0, 20)}...`); - // Reinstall native app - console.info(`→ Remove and install app from: ${getNativeAppPath()}`); + // Uninstall RN app + console.info('→ Removing legacy RN app...'); await driver.removeApp(getAppId()); resetBootedIOSKeychain(); + + // Install native app + console.info(`→ Installing native app from: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); await driver.activateApp(getAppId()); - // restore wallet and verify migration - await restoreWallet(MIGRATION_MNEMONIC, { reinstall: false }); + // Restore wallet with mnemonic (uses custom flow to handle backup sheet) + await restoreWallet(mnemonic, { reinstall: false, expectBackupSheet: true }); + + // Verify migration await verifyMigration(); }); - ciIt('@migration_2 - Install native app on top of legacy RN app', async () => { - await installLegacyRnApp(); - await restoreLegacyRnWallet(MIGRATION_MNEMONIC); + // -------------------------------------------------------------------------- + // Migration Scenario 2: Install native on top of RN (upgrade) + // -------------------------------------------------------------------------- + ciIt('@migration_2 - Install native app on top of RN app', async () => { + // Setup wallet in RN app + await setupLegacyWallet(); - // Install native app - console.info(`→ Installing app from: ${getNativeAppPath()}`); + // Install native app ON TOP of RN (upgrade) + console.info(`→ Installing native app on top of RN: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); await driver.activateApp(getAppId()); - // verify migration + // Handle migration flow await handleAndroidAlert(); await expectText('Migration Complete'); await dismissBackupTimedSheet(); + + // Verify migration await verifyMigration(); }); }); -async function installLegacyRnApp() { +// ============================================================================ +// WALLET SETUP HELPERS (RN App) +// ============================================================================ + +/** + * Complete wallet setup in legacy RN app: + * 1. Create new wallet + * 2. Fund with on-chain tx + * + * TODO: Add these steps once basic flow works: + * 3. Send on-chain tx (with tag) + * 4. Transfer to spending balance (create channel) + */ +async function setupLegacyWallet(): Promise { + console.info('=== Setting up legacy RN wallet ==='); + + // Install and create wallet + await installLegacyRnApp(); + await createLegacyRnWallet(); + + // 1. Fund wallet (receive on-chain) + console.info('→ Step 1: Funding wallet on-chain...'); + await fundRnWallet(INITIAL_FUND_SATS); + + // TODO: Add more steps once basic migration works + // // 2. Send on-chain tx with tag + // console.info('→ Step 2: Sending on-chain tx...'); + // await sendRnOnchainWithTag(ONCHAIN_SEND_SATS, TAG_SENT); + + // // 3. Transfer to spending (create channel via Blocktank) + // console.info('→ Step 3: Creating spending balance (channel)...'); + // await transferToSpending(TRANSFER_TO_SPENDING_SATS); + + console.info('=== Legacy wallet setup complete ==='); +} + +async function installLegacyRnApp(): Promise { + console.info(`→ Installing legacy RN app from: ${getRnAppPath()}`); await reinstallAppFromPath(getRnAppPath()); } -async function restoreLegacyRnWallet(seed: string) { +async function createLegacyRnWallet(): Promise { + console.info('→ Creating new wallet in legacy RN app...'); await elementById('Continue').waitForDisplayed(); await tap('Check1'); await tap('Check2'); await tap('Continue'); - await tap('SkipIntro'); - await tap('RestoreWallet'); - await tap('MultipleDevices-button'); - - await typeText('Word-0', seed); - await sleep(1500); - await tap('RestoreButton'); + // Create new wallet + await tap('NewWallet'); await waitForSetupWalletScreenFinish(); - const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed({ timeout: 120000 }); - await tap('GetStartedButton'); - await sleep(1000); - await expectText(totalBalance, { strategy: 'contains' }); - await expectText(savingBalance, { strategy: 'contains' }); - await expectText(spendingBalance, { strategy: 'contains' }); + // Wait for wallet to be created + for (let i = 1; i <= 3; i++) { + try { + await tap('WalletOnboardingClose'); + break; + } catch { + if (i === 3) throw new Error('Tapping "WalletOnboardingClose" timeout'); + } + } + console.info('→ Legacy RN wallet created'); } -async function verifyMigration() { - console.info('→ Verifying migrated wallet balances...'); - const totalBalanceEl = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); - await expect(totalBalanceEl).toHaveText(totalBalance); - await expectTextWithin('ActivitySpending', spendingBalance); - await expectTextWithin('ActivitySavings', savingBalance); +// ============================================================================ +// RN APP INTERACTION HELPERS +// ============================================================================ + +/** + * Get receive address from RN app (uses existing helper) + */ +async function getRnReceiveAddress(): Promise { + const address = await getReceiveAddress('bitcoin'); + console.info(`→ RN receive address: ${address}`); + await swipeFullScreen('down'); // close receive sheet + return address; +} + +/** + * Fund RN wallet with on-chain tx + */ +async function fundRnWallet(sats: number): Promise { + const address = await getRnReceiveAddress(); + + // Deposit and mine + await deposit(address, sats); + await mineBlocks(1); + await electrumClient?.waitForSync(); + + // Wait for balance to appear + await sleep(3000); + const expectedBalance = sats.toLocaleString('en').replace(/,/g, ' '); + await expectText(expectedBalance, { strategy: 'contains' }); + console.info(`→ Received ${sats} sats`); + + // Ensure we're back on main screen (dismiss any sheets/modals) + await swipeFullScreen('down'); + await sleep(500); +} + +/** + * Send on-chain tx from RN wallet and add a tag + */ +async function sendRnOnchainWithTag(sats: number, tag: string): Promise { + const externalAddress = await getExternalAddress(); + + // Use existing helper for address entry (handles camera permission) + await enterAddress(externalAddress); + + // Enter amount + const satsStr = String(sats); + for (const digit of satsStr) { + await tap(`N${digit}`); + } + await tap('ContinueAmount'); - console.info('→ Verify transaction details...'); + // Add tag before sending + await elementById('TagsAddSend').waitForDisplayed(); + await tap('TagsAddSend'); + await typeText('TagInputSend', tag); + await tap('TagsAddSend'); // confirm tag + + // Send + await dragOnElement('GRAB', 'right', 0.95); + await elementById('SendSuccess').waitForDisplayed(); + await tap('Close'); + + // Mine and sync + await mineBlocks(1); + await electrumClient?.waitForSync(); + await sleep(2000); + console.info(`→ Sent ${sats} sats with tag "${tag}"`); +} + +/** + * Transfer savings to spending balance (create channel via Blocktank) + */ +async function transferToSpending(sats: number): Promise { + // Navigate to transfer + await tap('Suggestion-lightning'); + await elementById('TransferIntro-button').waitForDisplayed(); + await tap('TransferIntro-button'); + await tap('FundTransfer'); + await tap('SpendingIntro-button'); + await sleep(2000); // let animation finish + + // Enter amount + const satsStr = String(sats); + for (const digit of satsStr) { + await tap(`N${digit}`); + } + await tap('SpendingAmountContinue'); + + // Confirm with default settings + await tap('SpendingConfirmDefault'); + await dragOnElement('GRAB', 'right', 0.95); + + // Wait for channel to be created + await elementById('TransferSuccess').waitForDisplayed({ timeout: 120000 }); + await tap('TransferSuccess'); + + // Mine blocks to confirm channel + await mineBlocks(6); + await electrumClient?.waitForSync(); + await sleep(5000); + console.info(`→ Created spending balance with ${sats} sats`); +} + +/** + * Tag the latest (most recent) transaction in the activity list + */ +async function tagLatestTransaction(tag: string): Promise { + // Go to activity await swipeFullScreen('up'); await swipeFullScreen('up'); await tap('ActivityShowAll'); - // All transactions - await expectTextWithin('Activity-1', '-'); - await expectTextWithin('Activity-2', '+'); - await expectTextWithin('Activity-3', '-'); - await expectTextWithin('Activity-4', '-'); - await expectTextWithin('Activity-5', '+'); - await expectTextWithin('Activity-6', '+'); - - // Sent - await tap('Tab-sent'); - await expectTextWithin('Activity-1', '-'); - await expectTextWithin('Activity-2', '-'); - await elementById('Activity-3').waitForDisplayed({ reverse: true }); - - // Received - await tap('Tab-received'); - await expectTextWithin('Activity-1', '+'); - await expectTextWithin('Activity-2', '+'); - await expectTextWithin('Activity-3', '+'); - await elementById('Activity-4').waitForDisplayed({ reverse: true }); - - // Other - await tap('Tab-other'); - await elementById('Activity-1').waitForDisplayed(); - await elementById('Activity-2').waitForDisplayed({ reverse: true }); + // Tap latest transaction + await tap('Activity-1'); - // filter by receive tag - await tap('Tab-all'); - await tap('TagsPrompt'); - await sleep(500); - await tap('Tag-received '); - await expectTextWithin('Activity-1', '+'); - await expectTextWithin('Activity-2', '+'); - await elementById('Activity-3').waitForDisplayed({ reverse: true }); - await tap('Tag-received -delete'); - - // filter by send tag - await tap('TagsPrompt'); + // Add tag + await elementById('ActivityTags').waitForDisplayed(); + await tap('ActivityTags'); + await typeText('TagInput', tag); + await tap('ActivityTagsSubmit'); + + // Go back + await doNavigationClose(); + await doNavigationClose(); + console.info(`→ Tagged latest transaction with "${tag}"`); +} + +/** + * Get mnemonic from RN wallet settings + */ +async function getRnMnemonic(): Promise { + // Navigate to backup settings + await tap('HeaderMenu'); + await sleep(500); // Wait for drawer to open + await elementById('DrawerSettings').waitForDisplayed(); + await tap('DrawerSettings'); + await elementById('BackupSettings').waitForDisplayed(); + await tap('BackupSettings'); + + // Tap "Backup Wallet" to show mnemonic screen + await elementById('BackupWallet').waitForDisplayed(); + await tap('BackupWallet'); + + // Show seed (note: typo in RN code is "SeedContaider") + await elementById('SeedContaider').waitForDisplayed(); + const seedElement = await elementById('SeedContaider'); + const seed = await getAccessibleText(seedElement); + + if (!seed) throw new Error('Could not read seed from "SeedContaider"'); + console.info(`→ RN mnemonic retrieved: ${seed.split(' ').slice(0, 3).join(' ')}...`); + + // Navigate back to main screen using Android back button + // ShowMnemonic -> BackupSettings -> Settings -> Main + await driver.back(); + await sleep(300); + await driver.back(); + await sleep(300); + await driver.back(); await sleep(500); - await tap('Tag-sent'); - await expectTextWithin('Activity-1', '-'); - await expectTextWithin('Activity-2', '-'); - await elementById('Activity-3').waitForDisplayed({ reverse: true }); - await tap('Tag-sent-delete'); + + return seed; +} + +// ============================================================================ +// MIGRATION VERIFICATION +// ============================================================================ + +/** + * Verify migration was successful (basic version - just checks balance) + */ +async function verifyMigration(): Promise { + console.info('=== Verifying migration ==='); + + // Verify we have balance (should match what we funded) + const totalBalanceEl = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); + const balanceText = await totalBalanceEl.getText(); + console.info(`→ Total balance: ${balanceText}`); + + // Basic check - we should have funds + const balanceNum = parseInt(balanceText.replace(/\s/g, ''), 10); + if (balanceNum <= 0) { + throw new Error(`Expected positive balance, got: ${balanceText}`); + } + console.info('→ Balance migrated successfully'); + + // Go to activity list to verify transactions exist + await swipeFullScreen('up'); + await swipeFullScreen('up'); + await tap('ActivityShowAll'); + + // Verify we have at least one transaction (the receive) + await elementById('Activity-1').waitForDisplayed(); + console.info('→ Transaction history migrated successfully'); await doNavigationClose(); + + console.info('=== Migration verified successfully ==='); } From 5cdbe5cbacb0e486878470ebd4a2b903694ced44 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 12 Jan 2026 11:57:41 +0100 Subject: [PATCH 18/39] additional scenarios with passphrase --- test/specs/migration.e2e.ts | 92 ++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index c78a026..5c16ecf 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,4 +1,5 @@ import { + confirmInputOnKeyboard, dismissBackupTimedSheet, doNavigationClose, dragOnElement, @@ -43,12 +44,14 @@ const INITIAL_FUND_SATS = 500_000; // 500k sats initial funding const ONCHAIN_SEND_SATS = 50_000; // 50k sats for on-chain send test const TRANSFER_TO_SPENDING_SATS = 100_000; // 100k for creating a channel +// Passphrase for passphrase-protected wallet tests +const TEST_PASSPHRASE = 'supersecret'; + // ============================================================================ // TEST SUITE // ============================================================================ describe('@migration - Migration from legacy RN app to native app', () => { - let mnemonic: string = ''; before(async () => { await ensureLocalFunds(); @@ -67,9 +70,8 @@ describe('@migration - Migration from legacy RN app to native app', () => { await setupLegacyWallet(); // Get mnemonic before uninstalling - mnemonic = await getRnMnemonic(); - console.info(`→ Got mnemonic: ${mnemonic.substring(0, 20)}...`); - + const mnemonic = await getRnMnemonic(); + await sleep(1000); // Uninstall RN app console.info('→ Removing legacy RN app...'); await driver.removeApp(getAppId()); @@ -107,6 +109,59 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Verify migration await verifyMigration(); }); + + // -------------------------------------------------------------------------- + // Migration Scenario 3: Uninstall RN, install Native, restore with passphrase + // -------------------------------------------------------------------------- + ciIt('@migration_3 - Uninstall RN app and install native app (with passphrase)', async () => { + // Setup wallet in RN app WITH passphrase + await setupLegacyWallet({ passphrase: TEST_PASSPHRASE }); + + // Get mnemonic before uninstalling + const mnemonic = await getRnMnemonic(); + await sleep(10000); + + // Uninstall RN app + console.info('→ Removing legacy RN app...'); + await driver.removeApp(getAppId()); + resetBootedIOSKeychain(); + + // Install native app + console.info(`→ Installing native app from: ${getNativeAppPath()}`); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // Restore wallet with mnemonic AND passphrase + await restoreWallet(mnemonic, { + reinstall: false, + expectBackupSheet: true, + passphrase: TEST_PASSPHRASE, + }); + + // Verify migration + await verifyMigration(); + }); + + // -------------------------------------------------------------------------- + // Migration Scenario 4: Install native on top of RN with passphrase (upgrade) + // -------------------------------------------------------------------------- + ciIt('@migration_4 - Install native app on top of RN app (with passphrase)', async () => { + // Setup wallet in RN app WITH passphrase + await setupLegacyWallet({ passphrase: TEST_PASSPHRASE }); + + // Install native app ON TOP of RN (upgrade) + console.info(`→ Installing native app on top of RN: ${getNativeAppPath()}`); + await driver.installApp(getNativeAppPath()); + await driver.activateApp(getAppId()); + + // Handle migration flow + await handleAndroidAlert(); + await expectText('Migration Complete'); + await dismissBackupTimedSheet(); + + // Verify migration + await verifyMigration(); + }); }); // ============================================================================ @@ -115,19 +170,20 @@ describe('@migration - Migration from legacy RN app to native app', () => { /** * Complete wallet setup in legacy RN app: - * 1. Create new wallet + * 1. Create new wallet (optionally with passphrase) * 2. Fund with on-chain tx * * TODO: Add these steps once basic flow works: * 3. Send on-chain tx (with tag) * 4. Transfer to spending balance (create channel) */ -async function setupLegacyWallet(): Promise { - console.info('=== Setting up legacy RN wallet ==='); +async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise { + const { passphrase } = options; + console.info(`=== Setting up legacy RN wallet${passphrase ? ' (with passphrase)' : ''} ===`); // Install and create wallet await installLegacyRnApp(); - await createLegacyRnWallet(); + await createLegacyRnWallet({ passphrase }); // 1. Fund wallet (receive on-chain) console.info('→ Step 1: Funding wallet on-chain...'); @@ -150,16 +206,27 @@ async function installLegacyRnApp(): Promise { await reinstallAppFromPath(getRnAppPath()); } -async function createLegacyRnWallet(): Promise { - console.info('→ Creating new wallet in legacy RN app...'); +async function createLegacyRnWallet(options: { passphrase?: string } = {}): Promise { + const { passphrase } = options; + console.info(`→ Creating new wallet in legacy RN app${passphrase ? ' (with passphrase)' : ''}...`); + await elementById('Continue').waitForDisplayed(); await tap('Check1'); await tap('Check2'); await tap('Continue'); await tap('SkipIntro'); + // Set passphrase if provided (before creating wallet) + if (passphrase) { + console.info('→ Setting passphrase...'); + await tap('Passphrase'); + await typeText('PassphraseInput', passphrase); + await confirmInputOnKeyboard(); + await tap('CreateNewWallet'); + } else { // Create new wallet - await tap('NewWallet'); + await tap('NewWallet'); + } await waitForSetupWalletScreenFinish(); // Wait for wallet to be created @@ -283,6 +350,7 @@ async function transferToSpending(sats: number): Promise { */ async function tagLatestTransaction(tag: string): Promise { // Go to activity + await sleep(1000); await swipeFullScreen('up'); await swipeFullScreen('up'); await tap('ActivityShowAll'); @@ -324,7 +392,7 @@ async function getRnMnemonic(): Promise { const seed = await getAccessibleText(seedElement); if (!seed) throw new Error('Could not read seed from "SeedContaider"'); - console.info(`→ RN mnemonic retrieved: ${seed.split(' ').slice(0, 3).join(' ')}...`); + console.info(`→ RN mnemonic retrieved: ${seed}...`); // Navigate back to main screen using Android back button // ShowMnemonic -> BackupSettings -> Settings -> Main From 17c5d6b703956df5174a2851627dd3e60611bc6e Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 12 Jan 2026 12:11:41 +0100 Subject: [PATCH 19/39] remove Migration Complete toast check --- test/specs/migration.e2e.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 5c16ecf..4ccd299 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -103,7 +103,6 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Handle migration flow await handleAndroidAlert(); - await expectText('Migration Complete'); await dismissBackupTimedSheet(); // Verify migration @@ -156,7 +155,6 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Handle migration flow await handleAndroidAlert(); - await expectText('Migration Complete'); await dismissBackupTimedSheet(); // Verify migration From 29260f3fcc50266cc6d61d8a6748baa892e525bb Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 12 Jan 2026 13:20:56 +0100 Subject: [PATCH 20/39] update titles --- test/specs/migration.e2e.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 4ccd299..906ca2b 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -65,7 +65,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { // -------------------------------------------------------------------------- // Migration Scenario 1: Uninstall RN, install Native, restore mnemonic // -------------------------------------------------------------------------- - ciIt('@migration_1 - Uninstall RN app and install native app', async () => { + ciIt('@migration_1 - Uninstall RN, install Native, restore mnemonic', async () => { // Setup wallet in RN app await setupLegacyWallet(); @@ -92,7 +92,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { // -------------------------------------------------------------------------- // Migration Scenario 2: Install native on top of RN (upgrade) // -------------------------------------------------------------------------- - ciIt('@migration_2 - Install native app on top of RN app', async () => { + ciIt('@migration_2 - Install native on top of RN (upgrade)', async () => { // Setup wallet in RN app await setupLegacyWallet(); @@ -112,7 +112,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { // -------------------------------------------------------------------------- // Migration Scenario 3: Uninstall RN, install Native, restore with passphrase // -------------------------------------------------------------------------- - ciIt('@migration_3 - Uninstall RN app and install native app (with passphrase)', async () => { + ciIt('@migration_3 - Uninstall RN, install Native, restore with passphrase', async () => { // Setup wallet in RN app WITH passphrase await setupLegacyWallet({ passphrase: TEST_PASSPHRASE }); @@ -144,7 +144,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { // -------------------------------------------------------------------------- // Migration Scenario 4: Install native on top of RN with passphrase (upgrade) // -------------------------------------------------------------------------- - ciIt('@migration_4 - Install native app on top of RN app (with passphrase)', async () => { + ciIt('@migration_4 - Install native on top of RN with passphrase (upgrade)', async () => { // Setup wallet in RN app WITH passphrase await setupLegacyWallet({ passphrase: TEST_PASSPHRASE }); From 01a73de866ab3a3a63bc4d9a2cc245920e7360bc Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 10:16:57 +0100 Subject: [PATCH 21/39] update readme and agents --- AGENTS.md | 22 +++++++++++++++++++++- README.md | 33 +++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2ab2c0f..a3e992d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,24 +46,39 @@ Notes: ## Running Tests +**Important:** The `BACKEND` env var controls which infrastructure the tests use for deposits/mining: + +- `BACKEND=local` (default) — Uses local docker stack (Bitcoin RPC on localhost:18443, Electrum on localhost:60001). Requires `bitkit-docker` running locally. +- `BACKEND=regtest` — Uses Blocktank API over the internet (remote regtest infrastructure). + +**The `BACKEND` must match how the app was built:** +- Apps built with `BACKEND=local` connect to localhost electrum → run tests with `BACKEND=local` +- Apps built with `BACKEND=regtest` connect to remote electrum → run tests with `BACKEND=regtest` + ```bash -# Android +# Android (local backend - default) npm run e2e:android +# Android (regtest backend - for apps built with BACKEND=regtest) +BACKEND=regtest npm run e2e:android + # iOS npm run e2e:ios +BACKEND=regtest npm run e2e:ios ``` Run a single spec: ```bash npm run e2e:android -- --spec ./test/specs/onboarding.e2e.ts +BACKEND=regtest npm run e2e:android -- --spec ./test/specs/migration.e2e.ts ``` Run by tag: ```bash npm run e2e:android -- --mochaOpts.grep "@backup" +BACKEND=regtest npm run e2e:android -- --mochaOpts.grep "@migration" ``` ## CI Helper Scripts @@ -71,8 +86,13 @@ npm run e2e:android -- --mochaOpts.grep "@backup" These wrap the `npm run e2e:*` commands and capture logs/artifacts: ```bash +# Local backend (default) ./ci_run_android.sh ./ci_run_ios.sh + +# Regtest backend +BACKEND=regtest ./ci_run_android.sh +BACKEND=regtest ./ci_run_ios.sh ``` ## Practical Tips diff --git a/README.md b/README.md index 636fcbb..f85675f 100644 --- a/README.md +++ b/README.md @@ -88,18 +88,34 @@ BACKEND=regtest ./scripts/build-ios-sim.sh ### 🧪 Running tests +**Important:** The `BACKEND` environment variable controls which infrastructure the tests use for blockchain operations (deposits, mining blocks): + +| Backend | Infrastructure | When to use | +|---------|---------------|-------------| +| `BACKEND=local` (default) | Local docker stack (Bitcoin RPC on localhost:18443, Electrum on localhost:60001) | Apps built with `BACKEND=local` | +| `BACKEND=regtest` | Blocktank API over the internet (remote regtest) | Apps built with `BACKEND=regtest` | + +> ⚠️ **The `BACKEND` must match how the app was built.** If the app connects to remote electrum, use `BACKEND=regtest`. If it connects to localhost, use `BACKEND=local`. + ```bash -# Run all tests on Android +# Run all tests on Android (local backend - default) npm run e2e:android +# Run all tests on Android (regtest backend) +BACKEND=regtest npm run e2e:android + # Run all tests on iOS npm run e2e:ios +BACKEND=regtest npm run e2e:ios ``` To run a **specific test file**: ```bash npm run e2e:android -- --spec ./test/specs/onboarding.e2e.ts + +# With regtest backend +BACKEND=regtest npm run e2e:android -- --spec ./test/specs/migration.e2e.ts ``` To run a **specific test case**: @@ -148,19 +164,25 @@ These helper scripts wrap the regular `npm run e2e:*` commands and add CI-friend The Android script will: - Clear and capture `adb logcat` output into `./artifacts/logcat.txt`. -- Reverse the regtest port (`60001`). +- Reverse the regtest port (`60001`) for local backend. - Run the Android E2E tests. - Forward any arguments directly to Mocha/WebdriverIO. **Usage examples:** ```bash -# Run all Android tests (with logcat capture) +# Run all Android tests with local backend (default) ./ci_run_android.sh +# Run all Android tests with regtest backend (for apps built with BACKEND=regtest) +BACKEND=regtest ./ci_run_android.sh + # Run only @backup tests ./ci_run_android.sh --mochaOpts.grep "@backup" +# Run migration tests (typically need regtest backend for RN app) +BACKEND=regtest ./ci_run_android.sh --mochaOpts.grep "@migration" + # Run backup OR onboarding OR onchain tests ./ci_run_android.sh --mochaOpts.grep "@backup|@onboarding|@onchain" @@ -183,9 +205,12 @@ The iOS helper mirrors the Android workflow but tailors it for the Apple Simulat **Usage examples:** ```bash -# Run all iOS tests (with simulator log capture) +# Run all iOS tests with local backend (default) ./ci_run_ios.sh +# Run all iOS tests with regtest backend +BACKEND=regtest ./ci_run_ios.sh + # Run only @onboarding-tagged tests ./ci_run_ios.sh --mochaOpts.grep "@onboarding" From f0a6c836f6b4f9df20a273d337b10e5aa1b6c35c Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 14:10:44 +0100 Subject: [PATCH 22/39] agents + readme --- AGENTS.md | 1 + README.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a3e992d..71cf3fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ Notes: - `BACKEND=regtest` — Uses Blocktank API over the internet (remote regtest infrastructure). **The `BACKEND` must match how the app was built:** + - Apps built with `BACKEND=local` connect to localhost electrum → run tests with `BACKEND=local` - Apps built with `BACKEND=regtest` connect to remote electrum → run tests with `BACKEND=regtest` diff --git a/README.md b/README.md index f85675f..157ed02 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,10 @@ BACKEND=regtest ./scripts/build-ios-sim.sh **Important:** The `BACKEND` environment variable controls which infrastructure the tests use for blockchain operations (deposits, mining blocks): -| Backend | Infrastructure | When to use | -|---------|---------------|-------------| -| `BACKEND=local` (default) | Local docker stack (Bitcoin RPC on localhost:18443, Electrum on localhost:60001) | Apps built with `BACKEND=local` | -| `BACKEND=regtest` | Blocktank API over the internet (remote regtest) | Apps built with `BACKEND=regtest` | +| Backend | Infrastructure | When to use | +| ------------------------- | -------------------------------------------------------------------------------- | --------------------------------- | +| `BACKEND=local` (default) | Local docker stack (Bitcoin RPC on localhost:18443, Electrum on localhost:60001) | Apps built with `BACKEND=local` | +| `BACKEND=regtest` | Blocktank API over the internet (remote regtest) | Apps built with `BACKEND=regtest` | > ⚠️ **The `BACKEND` must match how the app was built.** If the app connects to remote electrum, use `BACKEND=regtest`. If it connects to localhost, use `BACKEND=local`. From e62474f0f14eb46b29a17bb06e51be6c2e3e2ba4 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 14:11:01 +0100 Subject: [PATCH 23/39] formatting --- test/helpers/regtest.ts | 19 ++++++++++++------- test/specs/onchain.e2e.ts | 7 ++++++- test/specs/send.e2e.ts | 7 ++++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/test/helpers/regtest.ts b/test/helpers/regtest.ts index f9cb11e..95ed115 100644 --- a/test/helpers/regtest.ts +++ b/test/helpers/regtest.ts @@ -184,18 +184,23 @@ export async function getExternalAddress(): Promise { * @param address - The address to send to * @param amountBtcOrSats - Amount (BTC string for local, sats number for regtest) */ -export async function sendToAddress(address: string, amountBtcOrSats: string | number): Promise { +export async function sendToAddress( + address: string, + amountBtcOrSats: string | number +): Promise { const backend = getBackend(); if (backend === 'local') { const rpc = getRpc(); - const btc = typeof amountBtcOrSats === 'number' - ? (amountBtcOrSats / 100_000_000).toString() - : amountBtcOrSats; + const btc = + typeof amountBtcOrSats === 'number' + ? (amountBtcOrSats / 100_000_000).toString() + : amountBtcOrSats; return rpc.sendToAddress(address, btc); } else { - const sats = typeof amountBtcOrSats === 'string' - ? Math.round(parseFloat(amountBtcOrSats) * 100_000_000) - : amountBtcOrSats; + const sats = + typeof amountBtcOrSats === 'string' + ? Math.round(parseFloat(amountBtcOrSats) * 100_000_000) + : amountBtcOrSats; return blocktankDeposit(address, sats); } } diff --git a/test/specs/onchain.e2e.ts b/test/specs/onchain.e2e.ts index 2607081..55a0b74 100644 --- a/test/specs/onchain.e2e.ts +++ b/test/specs/onchain.e2e.ts @@ -24,7 +24,12 @@ import { acknowledgeReceivedPayment, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; -import { ensureLocalFunds, getExternalAddress, mineBlocks, sendToAddress } from '../helpers/regtest'; +import { + ensureLocalFunds, + getExternalAddress, + mineBlocks, + sendToAddress, +} from '../helpers/regtest'; describe('@onchain - Onchain', () => { let electrum: Awaited> | undefined; diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index ce2a1f8..df649e6 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -36,7 +36,12 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; -import { ensureLocalFunds, getBitcoinRpc, getExternalAddress, mineBlocks } from '../helpers/regtest'; +import { + ensureLocalFunds, + getBitcoinRpc, + getExternalAddress, + mineBlocks, +} from '../helpers/regtest'; describe('@send - Send', () => { let electrum: { waitForSync: any; stop: any }; From 5bb4a39175d268108d8317810d4848c29ce27a38 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 14:11:35 +0100 Subject: [PATCH 24/39] change waitForIdleTimeout --- wdio.conf.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/wdio.conf.ts b/wdio.conf.ts index 4aaedd9..c3e904a 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -66,6 +66,7 @@ export const config: WebdriverIO.Config = { 'appium:platformVersion': '13.0', 'appium:app': path.join(__dirname, 'aut', 'bitkit_e2e.apk'), 'appium:autoGrantPermissions': true, + 'appium:waitForIdleTimeout': 1000, } : { platformName: 'iOS', From 4ff907c2bb68a57ba6f60bb207691a118d52a1c3 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 14:11:52 +0100 Subject: [PATCH 25/39] adjust swipeFullScreen for RN --- test/helpers/actions.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 2a991e4..86103a5 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -330,12 +330,12 @@ export async function swipeFullScreen(direction: Direction) { endX = width * 0.8; break; case 'up': - startY = height * 0.8; + startY = height * 0.6; endY = height * 0.2; break; case 'down': startY = height * 0.2; - endY = height * 0.8; + endY = height * 0.6; break; } @@ -524,7 +524,12 @@ export async function restoreWallet( expectQuickPayTimedSheet = false, expectBackupSheet = false, reinstall = true, - }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; expectBackupSheet?: boolean; reinstall?: boolean } = {} + }: { + passphrase?: string; + expectQuickPayTimedSheet?: boolean; + expectBackupSheet?: boolean; + reinstall?: boolean; + } = {} ) { console.info('→ Restoring wallet with seed:', seed); // Let cloud state flush - carried over from Detox @@ -576,7 +581,7 @@ export async function restoreWallet( if (expectBackupSheet) { await dismissBackupTimedSheet(); } - + if (expectQuickPayTimedSheet) { await dismissQuickPayIntro(); } From 78000e2707160cb61cca8e641299af90a61c67cd Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 14:12:19 +0100 Subject: [PATCH 26/39] update migration scenarios to have sent tx and tags --- test/specs/migration.e2e.ts | 126 +++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 24 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 906ca2b..0290f1f 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -7,6 +7,7 @@ import { elementByIdWithin, enterAddress, expectText, + expectTextWithin, getAccessibleText, getReceiveAddress, handleAndroidAlert, @@ -52,7 +53,6 @@ const TEST_PASSPHRASE = 'supersecret'; // ============================================================================ describe('@migration - Migration from legacy RN app to native app', () => { - before(async () => { await ensureLocalFunds(); electrumClient = await initElectrum(); @@ -69,6 +69,10 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Setup wallet in RN app await setupLegacyWallet(); + //dismiss backup sheet if shown + await sleep(1000); + await swipeFullScreen('down'); + await sleep(500); // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); await sleep(1000); @@ -118,7 +122,6 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); - await sleep(10000); // Uninstall RN app console.info('→ Removing legacy RN app...'); @@ -186,12 +189,13 @@ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise // 1. Fund wallet (receive on-chain) console.info('→ Step 1: Funding wallet on-chain...'); await fundRnWallet(INITIAL_FUND_SATS); + await tagLatestTransaction(TAG_RECEIVED); - // TODO: Add more steps once basic migration works - // // 2. Send on-chain tx with tag - // console.info('→ Step 2: Sending on-chain tx...'); - // await sendRnOnchainWithTag(ONCHAIN_SEND_SATS, TAG_SENT); + // 2. Send on-chain tx with tag + console.info('→ Step 2: Sending on-chain tx...'); + await sendRnOnchainWithTag(ONCHAIN_SEND_SATS, TAG_SENT); + // TODO: Add transfer to spending once send works // // 3. Transfer to spending (create channel via Blocktank) // console.info('→ Step 3: Creating spending balance (channel)...'); // await transferToSpending(TRANSFER_TO_SPENDING_SATS); @@ -206,7 +210,9 @@ async function installLegacyRnApp(): Promise { async function createLegacyRnWallet(options: { passphrase?: string } = {}): Promise { const { passphrase } = options; - console.info(`→ Creating new wallet in legacy RN app${passphrase ? ' (with passphrase)' : ''}...`); + console.info( + `→ Creating new wallet in legacy RN app${passphrase ? ' (with passphrase)' : ''}...` + ); await elementById('Continue').waitForDisplayed(); await tap('Check1'); @@ -222,7 +228,7 @@ async function createLegacyRnWallet(options: { passphrase?: string } = {}): Prom await confirmInputOnKeyboard(); await tap('CreateNewWallet'); } else { - // Create new wallet + // Create new wallet await tap('NewWallet'); } await waitForSetupWalletScreenFinish(); @@ -236,6 +242,8 @@ async function createLegacyRnWallet(options: { passphrase?: string } = {}): Prom if (i === 3) throw new Error('Tapping "WalletOnboardingClose" timeout'); } } + await swipeFullScreen('up'); + await swipeFullScreen('down'); console.info('→ Legacy RN wallet created'); } @@ -276,28 +284,54 @@ async function fundRnWallet(sats: number): Promise { } /** - * Send on-chain tx from RN wallet and add a tag + * Send on-chain tx from RN wallet and add a tag. + * Note: This uses a custom flow for RN since camera permission is already granted from receive. */ async function sendRnOnchainWithTag(sats: number, tag: string): Promise { const externalAddress = await getExternalAddress(); - // Use existing helper for address entry (handles camera permission) - await enterAddress(externalAddress); + // RN-specific send flow (camera permission already granted during receive) + await tap('Send'); + await sleep(1000); + + // Tap manual address entry (skip camera since permission already granted) + await elementById('RecipientManual').waitForDisplayed(); + await tap('RecipientManual'); + + // Enter address + await elementById('RecipientInput').waitForDisplayed(); + await typeText('RecipientInput', externalAddress); + await confirmInputOnKeyboard(); + await sleep(500); + await tap('AddressContinue'); // Enter amount + await sleep(500); const satsStr = String(sats); for (const digit of satsStr) { await tap(`N${digit}`); } await tap('ContinueAmount'); - // Add tag before sending - await elementById('TagsAddSend').waitForDisplayed(); + // Wait for review screen + await sleep(1000); + + // Add tag - use click + addValue to trigger RN state update await tap('TagsAddSend'); - await typeText('TagInputSend', tag); - await tap('TagsAddSend'); // confirm tag + await elementById('TagInputSend').waitForDisplayed(); + const tagInput = await elementById('TagInputSend'); + await tagInput.click(); // Focus the input + await sleep(300); + // Use addValue to type (triggers RN onChangeText properly) + await tagInput.addValue(tag); + await sleep(300); + // Press Enter key to submit (keycode 66 = KEYCODE_ENTER) + await driver.pressKeyCode(66); + // Wait for tag sheet to close and return to Review screen + await sleep(1000); - // Send + // Send using swipe gesture + console.info(`→ About to send ${sats} sats with tag "${tag}"...`); await dragOnElement('GRAB', 'right', 0.95); await elementById('SendSuccess').waitForDisplayed(); await tap('Close'); @@ -357,14 +391,24 @@ async function tagLatestTransaction(tag: string): Promise { await tap('Activity-1'); // Add tag - await elementById('ActivityTags').waitForDisplayed(); - await tap('ActivityTags'); - await typeText('TagInput', tag); - await tap('ActivityTagsSubmit'); + await tap('ActivityTag'); + await elementById('TagInput').waitForDisplayed(); + const tagInput = await elementById('TagInput'); + await tagInput.click(); // Focus the input + await sleep(300); + // Use addValue to type (triggers RN onChangeText properly) + await tagInput.addValue(tag); + await sleep(300); + // Press Enter key to submit (keycode 66 = KEYCODE_ENTER) + await driver.pressKeyCode(66); + // Wait for tag sheet to close and return to Review screen + await sleep(1000); // Go back - await doNavigationClose(); - await doNavigationClose(); + await driver.back(); + await driver.back(); + await swipeFullScreen('down'); + await swipeFullScreen('down'); console.info(`→ Tagged latest transaction with "${tag}"`); } @@ -431,8 +475,42 @@ async function verifyMigration(): Promise { await swipeFullScreen('up'); await tap('ActivityShowAll'); - // Verify we have at least one transaction (the receive) - await elementById('Activity-1').waitForDisplayed(); + // All transactions + await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-2', '+'); + + // Sent, 2 transactions + await tap('Tab-sent'); + await expectTextWithin('Activity-1', '-'); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + + // Received, 2 transactions + await tap('Tab-received'); + await expectTextWithin('Activity-1', '+'); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + + // Other, 0 transactions + await tap('Tab-other'); + await elementById('Activity-1').waitForDisplayed({ reverse: true }); + + // filter by receive tag + await tap('Tab-all'); + await tap('TagsPrompt'); + await sleep(500); + await tap(`Tag-${TAG_RECEIVED}`); + await expectTextWithin('Activity-1', '+'); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + await tap(`Tag-${TAG_RECEIVED}-delete`); + + // filter by send tag + await tap('TagsPrompt'); + await sleep(500); + await tap(`Tag-${TAG_SENT}`); + await expectTextWithin('Activity-1', '-'); + await elementById('Activity-2').waitForDisplayed({ reverse: true }); + await tap(`Tag-${TAG_SENT}-delete`); + + console.info('→ Activity tags migrated successfully'); console.info('→ Transaction history migrated successfully'); await doNavigationClose(); From abda23954dd6b84f02f627677efb0c56ea954ce7 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 16:14:53 +0100 Subject: [PATCH 27/39] adjust swipe for RN with params --- test/helpers/actions.ts | 23 ++++++++++++++++++----- test/specs/migration.e2e.ts | 16 +++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 86103a5..64e1bad 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -312,7 +312,20 @@ export async function typeText(testId: string, text: string) { type Direction = 'left' | 'right' | 'up' | 'down'; -export async function swipeFullScreen(direction: Direction) { +export async function swipeFullScreen( + direction: Direction, + { + downStartYPercent = 0.2, + downEndYPercent = 0.8, + upStartYPercent = 0.8, + upEndYPercent = 0.2, + }: { + downStartYPercent?: number; + downEndYPercent?: number; + upStartYPercent?: number; + upEndYPercent?: number; + } = {} +) { const { width, height } = await driver.getWindowSize(); let startX = width / 2; @@ -330,12 +343,12 @@ export async function swipeFullScreen(direction: Direction) { endX = width * 0.8; break; case 'up': - startY = height * 0.6; - endY = height * 0.2; + startY = height * upStartYPercent; + endY = height * upEndYPercent; break; case 'down': - startY = height * 0.2; - endY = height * 0.6; + startY = height * downStartYPercent; + endY = height * downEndYPercent; break; } diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 0290f1f..d563364 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -71,7 +71,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { //dismiss backup sheet if shown await sleep(1000); - await swipeFullScreen('down'); + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); await sleep(500); // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); @@ -242,8 +242,6 @@ async function createLegacyRnWallet(options: { passphrase?: string } = {}): Prom if (i === 3) throw new Error('Tapping "WalletOnboardingClose" timeout'); } } - await swipeFullScreen('up'); - await swipeFullScreen('down'); console.info('→ Legacy RN wallet created'); } @@ -257,7 +255,7 @@ async function createLegacyRnWallet(options: { passphrase?: string } = {}): Prom async function getRnReceiveAddress(): Promise { const address = await getReceiveAddress('bitcoin'); console.info(`→ RN receive address: ${address}`); - await swipeFullScreen('down'); // close receive sheet + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); // close receive sheet return address; } @@ -279,7 +277,7 @@ async function fundRnWallet(sats: number): Promise { console.info(`→ Received ${sats} sats`); // Ensure we're back on main screen (dismiss any sheets/modals) - await swipeFullScreen('down'); + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); await sleep(500); } @@ -383,8 +381,8 @@ async function transferToSpending(sats: number): Promise { async function tagLatestTransaction(tag: string): Promise { // Go to activity await sleep(1000); - await swipeFullScreen('up'); - await swipeFullScreen('up'); + await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); await tap('ActivityShowAll'); // Tap latest transaction @@ -407,8 +405,8 @@ async function tagLatestTransaction(tag: string): Promise { // Go back await driver.back(); await driver.back(); - await swipeFullScreen('down'); - await swipeFullScreen('down'); + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); console.info(`→ Tagged latest transaction with "${tag}"`); } From f90fd7fd0dd45ed7e07622b04f8936cfbd7974c3 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 17:27:03 +0100 Subject: [PATCH 28/39] adjustments --- test/specs/migration.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index d563364..147e7fa 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -255,7 +255,7 @@ async function createLegacyRnWallet(options: { passphrase?: string } = {}): Prom async function getRnReceiveAddress(): Promise { const address = await getReceiveAddress('bitcoin'); console.info(`→ RN receive address: ${address}`); - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); // close receive sheet + await swipeFullScreen('down'); // close receive sheet return address; } @@ -277,7 +277,7 @@ async function fundRnWallet(sats: number): Promise { console.info(`→ Received ${sats} sats`); // Ensure we're back on main screen (dismiss any sheets/modals) - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + await swipeFullScreen('down'); await sleep(500); } From 56da31665e28d44097dbbd8ca89efadf7dd22de8 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 17:49:25 +0100 Subject: [PATCH 29/39] getRnMnemonic stability --- test/specs/migration.e2e.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 147e7fa..84fa70d 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -72,7 +72,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { //dismiss backup sheet if shown await sleep(1000); await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await sleep(500); + await sleep(2000); // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); await sleep(1000); @@ -415,9 +415,17 @@ async function tagLatestTransaction(tag: string): Promise { */ async function getRnMnemonic(): Promise { // Navigate to backup settings - await tap('HeaderMenu'); - await sleep(500); // Wait for drawer to open - await elementById('DrawerSettings').waitForDisplayed(); + try { + await tap('HeaderMenu'); + await sleep(500); // Wait for drawer to open + await elementById('DrawerSettings').waitForDisplayed({ timeout: 5000 }); + } catch { + console.info('→ Drawer did not open, trying again...'); + await tap('HeaderMenu'); + await sleep(500); // Wait for drawer to open + await elementById('DrawerSettings').waitForDisplayed({ timeout: 5000 }); + } + await tap('DrawerSettings'); await elementById('BackupSettings').waitForDisplayed(); await tap('BackupSettings'); From 00256a56dc083b0cf2ecdbf7b323b65a98348a54 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 17:58:59 +0100 Subject: [PATCH 30/39] sleep a bit to ensure RN backup --- test/specs/migration.e2e.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 84fa70d..c5dace0 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -440,7 +440,10 @@ async function getRnMnemonic(): Promise { const seed = await getAccessibleText(seedElement); if (!seed) throw new Error('Could not read seed from "SeedContaider"'); - console.info(`→ RN mnemonic retrieved: ${seed}...`); + console.info(`→ RN mnemonic retrieved: ${seed}`); + await swipeFullScreen('down'); // close mnemonic sheet + // wait for backup to be performed + await sleep(10000); // Navigate back to main screen using Android back button // ShowMnemonic -> BackupSettings -> Settings -> Main From c4429dd54e54f0cd345e38b2f6d23b64a6b4a973 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 19:29:06 +0100 Subject: [PATCH 31/39] tag tx after --- test/specs/migration.e2e.ts | 43 ++++++++++++++----------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index c5dace0..ee78908 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -69,10 +69,6 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Setup wallet in RN app await setupLegacyWallet(); - //dismiss backup sheet if shown - await sleep(1000); - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await sleep(2000); // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); await sleep(1000); @@ -122,7 +118,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Get mnemonic before uninstalling const mnemonic = await getRnMnemonic(); - + await sleep(1000); // Uninstall RN app console.info('→ Removing legacy RN app...'); await driver.removeApp(getAppId()); @@ -193,7 +189,8 @@ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise // 2. Send on-chain tx with tag console.info('→ Step 2: Sending on-chain tx...'); - await sendRnOnchainWithTag(ONCHAIN_SEND_SATS, TAG_SENT); + await sendRnOnchain(ONCHAIN_SEND_SATS); + await tagLatestTransaction(TAG_SENT); // TODO: Add transfer to spending once send works // // 3. Transfer to spending (create channel via Blocktank) @@ -285,7 +282,7 @@ async function fundRnWallet(sats: number): Promise { * Send on-chain tx from RN wallet and add a tag. * Note: This uses a custom flow for RN since camera permission is already granted from receive. */ -async function sendRnOnchainWithTag(sats: number, tag: string): Promise { +async function sendRnOnchain(sats: number): Promise { const externalAddress = await getExternalAddress(); // RN-specific send flow (camera permission already granted during receive) @@ -311,34 +308,19 @@ async function sendRnOnchainWithTag(sats: number, tag: string): Promise { } await tap('ContinueAmount'); - // Wait for review screen - await sleep(1000); - - // Add tag - use click + addValue to trigger RN state update - await tap('TagsAddSend'); - await elementById('TagInputSend').waitForDisplayed(); - const tagInput = await elementById('TagInputSend'); - await tagInput.click(); // Focus the input - await sleep(300); - // Use addValue to type (triggers RN onChangeText properly) - await tagInput.addValue(tag); - await sleep(300); - // Press Enter key to submit (keycode 66 = KEYCODE_ENTER) - await driver.pressKeyCode(66); - // Wait for tag sheet to close and return to Review screen - await sleep(1000); - // Send using swipe gesture - console.info(`→ About to send ${sats} sats with tag "${tag}"...`); + console.info(`→ About to send ${sats} sats...`); await dragOnElement('GRAB', 'right', 0.95); await elementById('SendSuccess').waitForDisplayed(); await tap('Close'); + await sleep(2000); // Mine and sync await mineBlocks(1); await electrumClient?.waitForSync(); - await sleep(2000); - console.info(`→ Sent ${sats} sats with tag "${tag}"`); + await sleep(1000); + await dismissSheet() + console.info(`→ Sent ${sats} sats`); } /** @@ -526,3 +508,10 @@ async function verifyMigration(): Promise { console.info('=== Migration verified successfully ==='); } + +async function dismissSheet(): Promise { + //dismiss a sheet if shown + await sleep(1000); + await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + await sleep(2000); +} From 4b43f20034e7fe2cd52cb10c6e81ea3a367d6341 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 19:41:23 +0100 Subject: [PATCH 32/39] simplify rn tagging --- test/specs/migration.e2e.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index ee78908..c400571 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -365,10 +365,7 @@ async function tagLatestTransaction(tag: string): Promise { await sleep(1000); await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await tap('ActivityShowAll'); - - // Tap latest transaction - await tap('Activity-1'); + await tap('ActivityShort-1'); // latest tx // Add tag await tap('ActivityTag'); @@ -386,7 +383,6 @@ async function tagLatestTransaction(tag: string): Promise { // Go back await driver.back(); - await driver.back(); await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); console.info(`→ Tagged latest transaction with "${tag}"`); From 83aefe54c81bfd70895cd2b095c655a1a495d99a Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 19:57:28 +0100 Subject: [PATCH 33/39] comments --- test/specs/migration.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index c400571..f118817 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -168,10 +168,10 @@ describe('@migration - Migration from legacy RN app to native app', () => { /** * Complete wallet setup in legacy RN app: * 1. Create new wallet (optionally with passphrase) - * 2. Fund with on-chain tx + * 2. Fund with on-chain tx (add tag to latest tx) + * 3. Send on-chain tx (add tag to latest tx) * * TODO: Add these steps once basic flow works: - * 3. Send on-chain tx (with tag) * 4. Transfer to spending balance (create channel) */ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise { From 877a3f31812ff5c1dfa43636a654a3d9b2916145 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 20:53:02 +0100 Subject: [PATCH 34/39] create blocktank channel --- test/specs/migration.e2e.ts | 95 +++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index f118817..107b06c 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -170,9 +170,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { * 1. Create new wallet (optionally with passphrase) * 2. Fund with on-chain tx (add tag to latest tx) * 3. Send on-chain tx (add tag to latest tx) - * - * TODO: Add these steps once basic flow works: - * 4. Transfer to spending balance (create channel) + * 4. Transfer to spending balance (create channel via Blocktank) */ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise { const { passphrase } = options; @@ -192,10 +190,9 @@ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise await sendRnOnchain(ONCHAIN_SEND_SATS); await tagLatestTransaction(TAG_SENT); - // TODO: Add transfer to spending once send works - // // 3. Transfer to spending (create channel via Blocktank) - // console.info('→ Step 3: Creating spending balance (channel)...'); - // await transferToSpending(TRANSFER_TO_SPENDING_SATS); + // 3. Transfer to spending (create channel via Blocktank) + console.info('→ Step 3: Creating spending balance (channel)...'); + await transferToSpending(TRANSFER_TO_SPENDING_SATS); console.info('=== Legacy wallet setup complete ==='); } @@ -327,13 +324,15 @@ async function sendRnOnchain(sats: number): Promise { * Transfer savings to spending balance (create channel via Blocktank) */ async function transferToSpending(sats: number): Promise { - // Navigate to transfer - await tap('Suggestion-lightning'); - await elementById('TransferIntro-button').waitForDisplayed(); - await tap('TransferIntro-button'); - await tap('FundTransfer'); - await tap('SpendingIntro-button'); - await sleep(2000); // let animation finish + // Navigate via ActivitySavings -> TransferToSpending + await tap('ActivitySavings'); + await elementById('TransferToSpending').waitForDisplayed(); + await tap('TransferToSpending'); + + // Handle intro screen if shown + await sleep(1000); + await tap('SpendingIntro-button'); // "Get Started" + await sleep(1000); // let animation finish // Enter amount const satsStr = String(sats); @@ -342,18 +341,49 @@ async function transferToSpending(sats: number): Promise { } await tap('SpendingAmountContinue'); - // Confirm with default settings - await tap('SpendingConfirmDefault'); + // Confirm screen - swipe to transfer (no intermediate button needed) + await sleep(1000); await dragOnElement('GRAB', 'right', 0.95); - // Wait for channel to be created - await elementById('TransferSuccess').waitForDisplayed({ timeout: 120000 }); - await tap('TransferSuccess'); + // Handle notification permission dialog if shown + await sleep(1000); + try { + const allowButton = await $('android=new UiSelector().text("Allow")'); + await allowButton.waitForDisplayed({ timeout: 5000 }); + await allowButton.click(); + } catch { + // Dialog might not appear, that's fine + } + + // RN shows "IN TRANSFER" screen - tap "Continue Using Bitkit" to dismiss and let it run in background + await sleep(2000); + try { + const continueButton = await $('android=new UiSelector().textContains("Continue")'); + await continueButton.waitForDisplayed({ timeout: 10000 }); + await continueButton.click(); + console.info('→ Dismissed transfer screen, continuing in background...'); + } catch { + // Screen might have auto-dismissed + } + + // Mine blocks periodically to progress the channel opening + console.info('→ Mining blocks to confirm channel...'); + for (let i = 0; i < 10; i++) { + await mineBlocks(3); + await sleep(5000); + // Check if spending balance shows the transferred amount (transfer complete) + try { + const expectedBalance = sats.toLocaleString('en').replace(/,/g, ' '); + await expectText(expectedBalance); + break; + } catch { + // Still waiting + } + } - // Mine blocks to confirm channel - await mineBlocks(6); await electrumClient?.waitForSync(); - await sleep(5000); + await sleep(3000); + await dismissSheet(); console.info(`→ Created spending balance with ${sats} sats`); } @@ -462,30 +492,33 @@ async function verifyMigration(): Promise { await swipeFullScreen('up'); await tap('ActivityShowAll'); - // All transactions - await expectTextWithin('Activity-1', '-'); - await expectTextWithin('Activity-2', '+'); + // All transactions (Transfer, Sent, Received = 3 items) + await expectTextWithin('Activity-1', '-'); // Transfer (spending) + await expectTextWithin('Activity-2', '-'); // Sent + await expectTextWithin('Activity-3', '+'); // Received - // Sent, 2 transactions + // Sent tab: should show Sent tx only (not Transfer) await tap('Tab-sent'); await expectTextWithin('Activity-1', '-'); await elementById('Activity-2').waitForDisplayed({ reverse: true }); - // Received, 2 transactions + // Received tab: should show Received tx only await tap('Tab-received'); await expectTextWithin('Activity-1', '+'); await elementById('Activity-2').waitForDisplayed({ reverse: true }); - // Other, 0 transactions + // Other tab: should show Transfer (spending) tx await tap('Tab-other'); - await elementById('Activity-1').waitForDisplayed({ reverse: true }); + await elementById('Activity-1').waitForDisplayed(); + await expectTextWithin('Activity-1', '-'); // Transfer shows here + await elementById('Activity-2').waitForDisplayed({ reverse: true }); // filter by receive tag await tap('Tab-all'); await tap('TagsPrompt'); await sleep(500); await tap(`Tag-${TAG_RECEIVED}`); - await expectTextWithin('Activity-1', '+'); + await expectTextWithin('Activity-1', '+'); // Only received tx has this tag await elementById('Activity-2').waitForDisplayed({ reverse: true }); await tap(`Tag-${TAG_RECEIVED}-delete`); @@ -493,7 +526,7 @@ async function verifyMigration(): Promise { await tap('TagsPrompt'); await sleep(500); await tap(`Tag-${TAG_SENT}`); - await expectTextWithin('Activity-1', '-'); + await expectTextWithin('Activity-1', '-'); // Only sent tx has this tag (not Transfer) await elementById('Activity-2').waitForDisplayed({ reverse: true }); await tap(`Tag-${TAG_SENT}-delete`); From e94dc039c464203de1fb9e1269f1ad0e76aa9f6d Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 21:29:47 +0100 Subject: [PATCH 35/39] tagLatestTransaction stability --- test/specs/migration.e2e.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 107b06c..6a63cdc 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -316,7 +316,7 @@ async function sendRnOnchain(sats: number): Promise { await mineBlocks(1); await electrumClient?.waitForSync(); await sleep(1000); - await dismissSheet() + await dismissSheet(); console.info(`→ Sent ${sats} sats`); } @@ -393,8 +393,14 @@ async function transferToSpending(sats: number): Promise { async function tagLatestTransaction(tag: string): Promise { // Go to activity await sleep(1000); - await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await swipeFullScreen('up', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + try { + await swipeFullScreen('up', { upStartYPercent: 0.6 }); + await swipeFullScreen('up', { upStartYPercent: 0.6 }); + await elementById('ActivityShort-1').waitForDisplayed({ timeout: 5000 }); + } catch { + await swipeFullScreen('up'); + await swipeFullScreen('up'); + } await tap('ActivityShort-1'); // latest tx // Add tag @@ -413,8 +419,8 @@ async function tagLatestTransaction(tag: string): Promise { // Go back await driver.back(); - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); + await swipeFullScreen('down', { downEndYPercent: 0.6 }); + await swipeFullScreen('down', { downEndYPercent: 0.6 }); console.info(`→ Tagged latest transaction with "${tag}"`); } @@ -539,8 +545,8 @@ async function verifyMigration(): Promise { } async function dismissSheet(): Promise { - //dismiss a sheet if shown - await sleep(1000); - await swipeFullScreen('down', { upStartYPercent: 0.6, downEndYPercent: 0.6 }); - await sleep(2000); + //dismiss a sheet if shown + await sleep(1000); + await swipeFullScreen('down', { downEndYPercent: 0.6 }); + await sleep(2000); } From 5454ab178765ca03a248b27dd128ad23ef5efedf Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 21:57:54 +0100 Subject: [PATCH 36/39] more try catch stability --- test/specs/migration.e2e.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 6a63cdc..a4bf3cf 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -325,6 +325,13 @@ async function sendRnOnchain(sats: number): Promise { */ async function transferToSpending(sats: number): Promise { // Navigate via ActivitySavings -> TransferToSpending + try { + await elementById('ActivitySavings').waitForDisplayed({ timeout: 5000 }); + } catch { + console.info('→ Scrolling to find ActivitySavings...'); + await swipeFullScreen('down', { downEndYPercent: 0.6 }); + await swipeFullScreen('down'); + } await tap('ActivitySavings'); await elementById('TransferToSpending').waitForDisplayed(); await tap('TransferToSpending'); @@ -398,6 +405,7 @@ async function tagLatestTransaction(tag: string): Promise { await swipeFullScreen('up', { upStartYPercent: 0.6 }); await elementById('ActivityShort-1').waitForDisplayed({ timeout: 5000 }); } catch { + console.info('→ Scrolling to find latest transaction...'); await swipeFullScreen('up'); await swipeFullScreen('up'); } From 9dc12ff458af605c9d52d523fd9fddf9b4b70bd7 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 22:39:54 +0100 Subject: [PATCH 37/39] setupLegacyWallet refactor to return mnemonic --- test/specs/migration.e2e.ts | 42 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index a4bf3cf..aa25a56 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -66,12 +66,9 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Migration Scenario 1: Uninstall RN, install Native, restore mnemonic // -------------------------------------------------------------------------- ciIt('@migration_1 - Uninstall RN, install Native, restore mnemonic', async () => { - // Setup wallet in RN app - await setupLegacyWallet(); + // Setup wallet in RN app and get mnemonic + const mnemonic = await setupLegacyWallet({ returnSeed: true }); - // Get mnemonic before uninstalling - const mnemonic = await getRnMnemonic(); - await sleep(1000); // Uninstall RN app console.info('→ Removing legacy RN app...'); await driver.removeApp(getAppId()); @@ -83,7 +80,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { await driver.activateApp(getAppId()); // Restore wallet with mnemonic (uses custom flow to handle backup sheet) - await restoreWallet(mnemonic, { reinstall: false, expectBackupSheet: true }); + await restoreWallet(mnemonic!, { reinstall: false, expectBackupSheet: true }); // Verify migration await verifyMigration(); @@ -113,12 +110,9 @@ describe('@migration - Migration from legacy RN app to native app', () => { // Migration Scenario 3: Uninstall RN, install Native, restore with passphrase // -------------------------------------------------------------------------- ciIt('@migration_3 - Uninstall RN, install Native, restore with passphrase', async () => { - // Setup wallet in RN app WITH passphrase - await setupLegacyWallet({ passphrase: TEST_PASSPHRASE }); + // Setup wallet in RN app WITH passphrase and get mnemonic + const mnemonic = await setupLegacyWallet({ passphrase: TEST_PASSPHRASE, returnSeed: true }); - // Get mnemonic before uninstalling - const mnemonic = await getRnMnemonic(); - await sleep(1000); // Uninstall RN app console.info('→ Removing legacy RN app...'); await driver.removeApp(getAppId()); @@ -130,7 +124,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { await driver.activateApp(getAppId()); // Restore wallet with mnemonic AND passphrase - await restoreWallet(mnemonic, { + await restoreWallet(mnemonic!, { reinstall: false, expectBackupSheet: true, passphrase: TEST_PASSPHRASE, @@ -171,9 +165,18 @@ describe('@migration - Migration from legacy RN app to native app', () => { * 2. Fund with on-chain tx (add tag to latest tx) * 3. Send on-chain tx (add tag to latest tx) * 4. Transfer to spending balance (create channel via Blocktank) + * + * @param options.passphrase - Optional passphrase for the wallet + * @param options.returnSeed - If true, returns the mnemonic seed + * @returns The mnemonic seed if returnSeed is true, otherwise undefined */ -async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise { - const { passphrase } = options; +async function setupLegacyWallet( + options: { + passphrase?: string; + returnSeed?: boolean; + } = {} +): Promise { + const { passphrase, returnSeed } = options; console.info(`=== Setting up legacy RN wallet${passphrase ? ' (with passphrase)' : ''} ===`); // Install and create wallet @@ -195,6 +198,13 @@ async function setupLegacyWallet(options: { passphrase?: string } = {}): Promise await transferToSpending(TRANSFER_TO_SPENDING_SATS); console.info('=== Legacy wallet setup complete ==='); + + // Get mnemonic if requested + if (returnSeed) { + const mnemonic = await getRnMnemonic(); + await sleep(1000); + return mnemonic; + } } async function installLegacyRnApp(): Promise { @@ -376,8 +386,7 @@ async function transferToSpending(sats: number): Promise { // Mine blocks periodically to progress the channel opening console.info('→ Mining blocks to confirm channel...'); for (let i = 0; i < 10; i++) { - await mineBlocks(3); - await sleep(5000); + await mineBlocks(1); // Check if spending balance shows the transferred amount (transfer complete) try { const expectedBalance = sats.toLocaleString('en').replace(/,/g, ' '); @@ -385,6 +394,7 @@ async function transferToSpending(sats: number): Promise { break; } catch { // Still waiting + await sleep(3000); } } From a1138e81aaaeaafef9e592b5b99288e66e07e21c Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 13 Jan 2026 23:06:32 +0100 Subject: [PATCH 38/39] remove redundant handleAndroidWallet --- test/helpers/actions.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 64e1bad..6170641 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -582,8 +582,6 @@ export async function restoreWallet( await tap('RestoreButton'); await waitForSetupWalletScreenFinish(); - await handleAndroidAlert(); - // Wait for Get Started const getStarted = await elementById('GetStartedButton'); await getStarted.waitForDisplayed({ timeout: 120000 }); From 06fcebfc6dfeab621fe0e78e7a2c34a6cfd7e2d7 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 14 Jan 2026 09:54:20 +0100 Subject: [PATCH 39/39] get mnemonic in the beginning --- test/specs/migration.e2e.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index aa25a56..177dcfa 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -183,6 +183,13 @@ async function setupLegacyWallet( await installLegacyRnApp(); await createLegacyRnWallet({ passphrase }); + let mnemonic: string | undefined; + if (returnSeed) { + // Get mnemonic for later restoration + mnemonic = await getRnMnemonic(); + console.info(`→ Legacy RN wallet mnemonic: ${mnemonic}`); + } + // 1. Fund wallet (receive on-chain) console.info('→ Step 1: Funding wallet on-chain...'); await fundRnWallet(INITIAL_FUND_SATS); @@ -199,10 +206,7 @@ async function setupLegacyWallet( console.info('=== Legacy wallet setup complete ==='); - // Get mnemonic if requested if (returnSeed) { - const mnemonic = await getRnMnemonic(); - await sleep(1000); return mnemonic; } }