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-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/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" diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index f1eb58b..2a991e4 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); @@ -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}"` ); } } @@ -522,14 +522,19 @@ export async function restoreWallet( { passphrase, expectQuickPayTimedSheet = false, - }: { passphrase?: string; expectQuickPayTimedSheet?: boolean } = {} + expectBackupSheet = false, + reinstall = true, + }: { passphrase?: string; expectQuickPayTimedSheet?: boolean; expectBackupSheet?: 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(); @@ -563,11 +568,15 @@ 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(); + if (expectBackupSheet) { + await dismissBackupTimedSheet(); + } + if (expectQuickPayTimedSheet) { await dismissQuickPayIntro(); } @@ -642,37 +651,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/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 { diff --git a/test/helpers/regtest.ts b/test/helpers/regtest.ts new file mode 100644 index 0000000..f9cb11e --- /dev/null +++ b/test/helpers/regtest.ts @@ -0,0 +1,249 @@ +/** + * 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'; // Use || to handle empty string + 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(); +} + +/** + * 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. + * + * @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/helpers/setup.ts b/test/helpers/setup.ts index 911b7a0..488f0d0 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -27,18 +27,18 @@ 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}` - ); + throw new Error(`RN APK not found at: ${appPath}. Set RN_APK_PATH or place it at ${fallback}`); } return appPath; } 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( @@ -61,7 +61,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/backup.e2e.ts b/test/specs/backup.e2e.ts index 3ea60d6..2d5c9ca 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,21 +17,13 @@ import { waitForBackup, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; +import { ensureLocalFunds } from '../helpers/regtest'; describe('@backup - Backup', () => { let electrum: Awaited> | undefined; - const rpc = new BitcoinJsonRpc(bitcoinURL); 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(); }); @@ -57,7 +47,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..7fb276e 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,24 +17,16 @@ 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 { ensureLocalFunds, getExternalAddress, mineBlocks } from '../helpers/regtest'; describe('@boost - Boost', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); 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(); }); @@ -52,7 +41,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 +114,7 @@ describe('@boost - Boost', () => { await elementById('StatusBoosting').waitForDisplayed(); // mine new block - await mineBlocks(rpc, 1); + await mineBlocks(1); await doNavigationClose(); await sleep(500); @@ -142,10 +131,10 @@ 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(); + const coreAddress = await getExternalAddress(); await enterAddress(coreAddress); await tap('N1'); await tap('N0'); @@ -231,7 +220,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..3671f15 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,20 +38,16 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; +import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; describe('@lightning - Lightning', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + // 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(); }); @@ -77,7 +71,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 +287,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..1ba6586 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 { 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 = new BitcoinJsonRpc(bitcoinURL); + // 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 @@ -105,7 +100,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(); @@ -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/migration.e2e.ts b/test/specs/migration.e2e.ts index 8d03450..906ca2b 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -1,47 +1,441 @@ -import { elementById, restoreWallet, sleep, tap, typeText, waitForSetupWalletScreenFinish } from '../helpers/actions'; +import { + confirmInputOnKeyboard, + dismissBackupTimedSheet, + doNavigationClose, + dragOnElement, + elementById, + elementByIdWithin, + enterAddress, + expectText, + getAccessibleText, + getReceiveAddress, + 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'; +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; -describe('@migration - Legacy RN migration', () => { - ciIt('@migration_1 - Can restore legacy RN wallet from mnemonic', async () => { - await installLegacyRnApp(); - await restoreLegacyRnWallet(MIGRATION_MNEMONIC); +// ============================================================================ +// MIGRATION TEST CONFIGURATION +// ============================================================================ - // Restore into native app - // await installNativeApp(); - await restoreWallet(MIGRATION_MNEMONIC); +// 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 + +// Passphrase for passphrase-protected wallet tests +const TEST_PASSPHRASE = 'supersecret'; + +// ============================================================================ +// TEST SUITE +// ============================================================================ + +describe('@migration - Migration from legacy RN app to native app', () => { + + 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, install Native, restore mnemonic', async () => { + // Setup wallet in RN app + await setupLegacyWallet(); + + // Get mnemonic before uninstalling + const mnemonic = await getRnMnemonic(); + await sleep(1000); + // 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 (uses custom flow to handle backup sheet) + await restoreWallet(mnemonic, { reinstall: false, expectBackupSheet: true }); + + // Verify migration + await verifyMigration(); + }); + + // -------------------------------------------------------------------------- + // Migration Scenario 2: Install native on top of RN (upgrade) + // -------------------------------------------------------------------------- + ciIt('@migration_2 - Install native on top of RN (upgrade)', async () => { + // Setup wallet in RN app + await setupLegacyWallet(); + + // 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 dismissBackupTimedSheet(); + + // Verify migration + await verifyMigration(); + }); + + // -------------------------------------------------------------------------- + // 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 }); + + // 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 on top of RN with passphrase (upgrade)', 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 dismissBackupTimedSheet(); + + // Verify migration + await verifyMigration(); }); }); -async function installNativeApp() { - await reinstallAppFromPath(getNativeAppPath()); +// ============================================================================ +// WALLET SETUP HELPERS (RN App) +// ============================================================================ + +/** + * Complete wallet setup in legacy RN app: + * 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(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({ passphrase }); + + // 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() { + +async function installLegacyRnApp(): Promise { + console.info(`→ Installing legacy RN app from: ${getRnAppPath()}`); await reinstallAppFromPath(getRnAppPath()); } -async function restoreLegacyRnWallet(seed: string) { +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'); - await tap('RestoreWallet'); - await tap('MultipleDevices-button'); - - await typeText('Word-0', seed); - await sleep(1500); - await tap('RestoreButton'); + // 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 waitForSetupWalletScreenFinish(); - const getStarted = await elementById('GetStartedButton'); - await getStarted.waitForDisplayed( { timeout: 120000 }); - await tap('GetStartedButton'); + // 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'); +} + +// ============================================================================ +// 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'); + + // 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 sleep(1000); + await swipeFullScreen('up'); + await swipeFullScreen('up'); + await tap('ActivityShowAll'); + + // Tap latest transaction + await tap('Activity-1'); + + // 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}...`); + + // 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); + + 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 ==='); } diff --git a/test/specs/onchain.e2e.ts b/test/specs/onchain.e2e.ts index 787d701..2607081 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,21 +24,13 @@ import { acknowledgeReceivedPayment, } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; +import { ensureLocalFunds, getExternalAddress, mineBlocks, sendToAddress } from '../helpers/regtest'; describe('@onchain - Onchain', () => { let electrum: Awaited> | undefined; - const rpc = new BitcoinJsonRpc(bitcoinURL); 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(); }); @@ -57,10 +46,10 @@ 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(); + const coreAddress = await getExternalAddress(); console.info({ coreAddress }); await enterAddress(coreAddress); await tap('N1'); @@ -72,7 +61,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]; @@ -124,10 +113,10 @@ describe('@onchain - Onchain', () => { await tap('ShowQrReceive'); await swipeFullScreen('down'); - await rpc.sendToAddress(address, '1'); + await sendToAddress(address, '1'); await acknowledgeReceivedPayment(); - await mineBlocks(rpc, 1); + await mineBlocks(1); await electrum?.waitForSync(); await sleep(1000); // wait for the app to settle @@ -143,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 @@ -166,7 +155,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 +247,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'); @@ -267,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); @@ -294,7 +283,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..e67e0d5 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,24 +11,16 @@ 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 { ensureLocalFunds, getExternalAddress } from '../helpers/regtest'; describe('@security - Security And Privacy', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); 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(); }); @@ -80,10 +70,10 @@ describe('@security - Security And Privacy', () => { await elementById('TotalBalance').waitForDisplayed(); // receive - await receiveOnchainFunds(rpc); + 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 f7bbb27..ce2a1f8 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,20 +36,16 @@ import { checkChannelStatus, } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; +import { ensureLocalFunds, getBitcoinRpc, getExternalAddress, mineBlocks } from '../helpers/regtest'; describe('@send - Send', () => { let electrum: { waitForSync: any; stop: any }; - const rpc = new BitcoinJsonRpc(bitcoinURL); + // 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(); }); @@ -96,14 +90,14 @@ describe('@send - Send', () => { // Receive funds and check validation w/ balance await swipeFullScreen('down'); - await receiveOnchainFunds(rpc); + await receiveOnchainFunds(); await tap('Send'); await sleep(500); await tap('RecipientManual'); // check validation for address - const address2 = await rpc.getNewAddress(); + const address2 = await getExternalAddress(); try { await typeAddressAndVerifyContinue({ address: address2 }); } catch { @@ -143,7 +137,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); @@ -356,7 +350,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; } @@ -396,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' }); @@ -404,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" @@ -471,7 +469,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..42b6081 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,24 +30,20 @@ import { waitForActiveChannel, waitForPeerConnection, } from '../helpers/lnd'; -import { bitcoinURL, lndConfig } from '../helpers/constants'; +import { lndConfig } from '../helpers/constants'; +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 = new BitcoinJsonRpc(bitcoinURL); + // 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(); }); @@ -77,7 +70,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 +295,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 +355,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); 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, } : {