Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 6 additions & 42 deletions test/helpers/native-migration.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,27 @@
import fs from 'node:fs';
import path from 'node:path';
import {
acknowledgeReceivedPayment,
completeOnboarding,
confirmInputOnKeyboard,
doNavigationClose,
elementById,
expectTextWithin,
expectSavingsBalance,
expectSpendingBalance,
getReceiveAddress,
getSavingsBalance,
getSeed,
getSpendingBalance,
getTotalBalance,
receiveOnchainFunds,
restoreWallet,
swipeFullScreen,
tap,
transferSavingsToSpending,
typeText,
waitForBackup,
waitForWalletHome,
} from './actions';
import { getAppId, getBackend } from './constants';
import type { ElectrumClient } from './electrum';
import { payInvoice } from './regtest';
import { getNativeAppPath, grantIOSCameraPermission, reinstallAppFromPath } from './setup';

const FUNDING_SATS = 200_000;
const SPENDING_SATS = 50_000;
const PAYMENT_SATS = 1_000;
const DEPOSIT_TAG = 'e2e';

export type MigrationBalances = { savings: number; spending: number; total: number };
Expand Down Expand Up @@ -94,7 +85,7 @@ export async function prepareNativeMigrationWallet(
await receiveOnchainFunds({ sats: FUNDING_SATS, verifyBalances: true });
await electrum.waitForSync();

// Tag the sole deposit before creating the transfer; row order is unambiguous here.
// Tag the sole deposit; row order is unambiguous here.
await tap('ActivitySavings');
await tap('Activity-1');
await tap('ActivityTag');
Expand All @@ -103,22 +94,16 @@ export async function prepareNativeMigrationWallet(
if (driver.isAndroid) await confirmInputOnKeyboard();
await returnToWalletHome();

await transferSavingsToSpending({
amountSats: SPENDING_SATS,
waitForSync: electrum.waitForSync,
triggerBackgroundPaymentsIntro: true,
});
await expectSpendingBalance(SPENDING_SATS, { timeout: 180_000 });
await expectSavingsBalance(0, { condition: 'gt' });
// No spending balance: Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168).
let balances = await readMigrationBalances();
try {
await driver.waitUntil(
async () => {
balances = await readMigrationBalances();
return (
balances.savings > 0 &&
balances.spending === SPENDING_SATS &&
balances.total === balances.savings + balances.spending
balances.savings === FUNDING_SATS &&
balances.spending === 0 &&
balances.total === FUNDING_SATS
);
},
{ timeout: 30_000, interval: 1_000, timeoutMsg: 'Source balances did not settle' }
Expand Down Expand Up @@ -200,8 +185,6 @@ async function verifyNativeHistory() {
await expectTextWithin('Activity-1', '+');
await elementById('Activity-2').waitForDisplayed({ reverse: true });
await tap(`Tag-${DEPOSIT_TAG}-delete`);
await tap('Tab-other');
await expectTextWithin('Activity-1', '-');
await doNavigationClose();
}

Expand All @@ -213,31 +196,12 @@ export async function verifyNativeMigration(
recordStage(method, 'target-recovered', await readMigrationBalances());
await verifyNativeHistory();

// A live payment checks channel usability, beyond a cached spending balance.
// The regtest helper pays into the wallet; outbound payment is covered separately.
const invoice = await getReceiveAddress('lightning');
await swipeFullScreen('down');
await payInvoice(invoice, PAYMENT_SATS);
await acknowledgeReceivedPayment();
const afterPayment = {
savings: balances.savings,
spending: balances.spending + PAYMENT_SATS,
total: balances.total + PAYMENT_SATS,
};
await expectMigrationBalances(afterPayment);
recordStage(method, 'payment-received', await readMigrationBalances());
await waitForBackup();
await driver.terminateApp(getAppId());
await driver.activateApp(getAppId());
await expectMigrationBalances(afterPayment);
await expectMigrationBalances(balances);
recordStage(method, 'relaunch-verified', await readMigrationBalances());
await verifyNativeHistory();
await tap('ActivityShowAll');
await tap('Tab-received');
// The only new received transaction is the live Lightning payment.
await expectTextWithin('Activity-1', '1 000');
await expectTextWithin('Activity-1', '+');
await doNavigationClose();
}

function recordStage(method: string, stage: string, balances?: MigrationBalances) {
Expand Down
3 changes: 2 additions & 1 deletion test/specs/hardware-wallet.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ describe('@hardware_wallet @ios_gate - Hardware Wallet', () => {
await expectSpendingBalance(0);
});

ciIt('@hardware_wallet_3 - Can transfer hardware wallet funds to spending', async () => {
// Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168)
ciIt.skip('@hardware_wallet_3 - Can transfer hardware wallet funds to spending', async () => {
const fundingSats = 100_000;
const transferSats = 20_000;

Expand Down
120 changes: 13 additions & 107 deletions test/specs/migration.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const TAG_SENT = 'sent';
const INITIAL_FUND_SATS = 500_000; // 500k sats initial funding
const ONCHAIN_SEND_SATS = 50_000; // 50k sats for on-chain send test
// const CJIT_INVOICE_SATS = 3000; // Unused - Blocktank regtest doesn't support CJIT
const TRANSFER_TO_SPENDING_SATS = 100_000; // 100k for creating a channel

// Passphrase for passphrase-protected wallet tests
const TEST_PASSPHRASE = 'supersecret';
Expand Down Expand Up @@ -317,7 +316,8 @@ async function getRnTotalBalance(): Promise<number> {
* 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)
* 4. Transfer to spending balance (create channel via Blocktank)
*
* No spending balance: Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168).
*
* On iOS: RN app has poor Appium support for wallet creation, so we restore
* from mnemonic/balance provided via env vars (RN_MNEMONIC, RN_BALANCE).
Expand Down Expand Up @@ -391,18 +391,10 @@ async function setupLegacyWallet(
await sendRnOnchain(ONCHAIN_SEND_SATS);
await tagLatestTransaction(TAG_SENT);

// 3. Transfer to spending (create channel via Blocktank)
console.info('→ Step 3: Creating spending balance (channel)...');
await transferToSpendingRN(TRANSFER_TO_SPENDING_SATS);

// Verify spending balance is visible (channel created)
console.info('→ Verifying spending balance is visible...');
await assertRnSpendingBalanceVisible(TRANSFER_TO_SPENDING_SATS);

// Get final balance before migration
const balance = await getRnTotalBalance();

await waitForRnBackup();
await waitForRnBackup(false);
console.info('=== Legacy wallet setup complete ===');

// Output for iOS CI to capture
Expand Down Expand Up @@ -857,64 +849,6 @@ async function sendRnOnchain(
console.info(`→ Sent ${sats} sats`);
}

/**
* Transfer savings to spending balance (create channel via Blocktank)
*/
async function transferToSpendingRN(sats: number, existingBalance = 0): Promise<void> {
// Navigate via ActivitySavings -> TransferToSpending
// ActivitySavings should be visible near the top of the wallet screen
try {
await elementById('ActivitySavings').waitForDisplayed({ timeout: 5000 });
} catch {
console.info('→ Scrolling to find ActivitySavings...');
// Scroll down to reveal ActivitySavings if hidden
await swipeFullScreenRN('down');
}
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);
for (const digit of satsStr) {
await tap(`N${digit}`);
}
await tap('SpendingAmountContinue');

// Confirm screen - swipe to transfer (no intermediate button needed)
await sleep(1000);
await dragOnElement('GRAB', 'right', 0.95);
await sleep(5000);
await handleAndroidAlert();

// Mine blocks periodically to progress the channel opening
console.info('→ Mining blocks to confirm channel...');
for (let i = 0; i < 10; i++) {
await mineBlocks(1);
// Check if spending balance shows the transferred amount (transfer complete)
try {
await elementById('TransferSuccess-button').waitForDisplayed();
await sleep(1000);
break;
} catch {
console.info('→ Transfer successful screen did not appear, waiting...');
}
}

await sleep(3000);
await tap('TransferSuccess-button');
await electrumClient?.waitForSync();
await sleep(3000);
const expectedBalance = (existingBalance + sats).toLocaleString('en').replace(/,/g, ' ');
await expectText(expectedBalance);
console.info(`→ Created spending balance with ${sats} sats`);
}

/**
* Tag the latest (most recent) transaction in the activity list.
*
Expand Down Expand Up @@ -1033,32 +967,6 @@ async function tapActivityAndWaitForTag(activityId: string): Promise<void> {
}
}

/**
* Assert that spending balance is visible in RN app.
* Waits for ActivitySpending to show the expected amount.
*/
async function assertRnSpendingBalanceVisible(expectedSats: number): Promise<void> {
const expectedText = expectedSats.toLocaleString('en').replace(/,/g, ' ');
for (let attempt = 1; attempt <= 10; attempt++) {
try {
await elementById('ActivitySpending').waitForDisplayed({ timeout: 5_000 });
const spendingEl = await elementById('ActivitySpending');
const text = await getAccessibleText(spendingEl);
if (text.includes(expectedText.replace(/\s/g, '')) || text.includes(expectedText)) {
console.info(`→ Spending balance confirmed: ${text}`);
return;
}
console.info(
`→ Spending balance check (attempt ${attempt}/10): "${text}" vs "${expectedText}"`
);
} catch {
console.info(`→ ActivitySpending not visible yet (attempt ${attempt}/10)`);
}
await sleep(2_000);
}
throw new Error(`RN source spending balance did not reach ${expectedSats} sats`);
}

/** Verify the final metadata and channel backups before exporting or migrating a source wallet. */
async function waitForRnBackup(requireConnections = true): Promise<void> {
await tap('HeaderMenu');
Expand Down Expand Up @@ -1198,19 +1106,19 @@ async function verifyMigration(

await expectMigrationBalances({
total: expectedBalance,
spending: TRANSFER_TO_SPENDING_SATS,
savings: expectedBalance - TRANSFER_TO_SPENDING_SATS,
spending: 0,
savings: expectedBalance,
});

// Go to activity list to verify transactions exist
await tap('ActivityShowAll');

// All transactions (Transfer, Sent, Received = 3 items)
await expectTextWithin('Activity-1', '-'); // Transfer (spending)
await expectTextWithin('Activity-2', '-'); // Sent
await expectTextWithin('Activity-3', '+'); // Received
// All transactions (Sent, Received = 2 items)
await expectTextWithin('Activity-1', '-'); // Sent
await expectTextWithin('Activity-2', '+'); // Received
await elementById('Activity-3').waitForDisplayed({ reverse: true });

// Sent tab: should show Sent tx only (not Transfer)
// Sent tab: should show Sent tx only
await tap('Tab-sent');
await expectTextWithin('Activity-1', '-');
await elementById('Activity-2').waitForDisplayed({ reverse: true });
Expand All @@ -1220,11 +1128,9 @@ async function verifyMigration(
await expectTextWithin('Activity-1', '+');
await elementById('Activity-2').waitForDisplayed({ reverse: true });

// Other tab: should show Transfer (spending) tx
// Other tab: no transfers without a spending balance
await tap('Tab-other');
await elementById('Activity-1').waitForDisplayed();
await expectTextWithin('Activity-1', '-'); // Transfer shows here
await elementById('Activity-2').waitForDisplayed({ reverse: true });
await elementById('Activity-1').waitForDisplayed({ reverse: true });

if (verifyTags) {
// filter by receive tag (metadata restore can lag Electrum balance after mnemonic restore)
Expand All @@ -1238,7 +1144,7 @@ async function verifyMigration(
// filter by send tag
await openTagFilter(TAG_SENT);
await tap(`Tag-${TAG_SENT}`);
await expectTextWithin('Activity-1', '-'); // Only sent tx has this tag (not Transfer)
await expectTextWithin('Activity-1', '-'); // Only sent tx has this tag
await elementById('Activity-2').waitForDisplayed({ reverse: true });
await tap(`Tag-${TAG_SENT}-delete`);

Expand Down
3 changes: 2 additions & 1 deletion test/specs/multiaddress.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ describe('@multi_address - Multi address', () => {
await expect(spendingBalanceAfter).toEqual(0);
});

ciIt(
// Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168)
ciIt.skip(
'@multi_address_2, @multi_address_staging, @staging - Receive to each address type, transfer all to spending, close channel to taproot',
async () => {
const addressTypes: addressTypePreference[] = ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh', 'p2tr'];
Expand Down
6 changes: 4 additions & 2 deletions test/specs/transfer.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ describe('@transfer - Transfer', () => {
electrum?.stop();
});

ciIt(
// Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168)
ciIt.skip(
'@transfer_1, @transfer_staging, @staging - Can buy a channel from Blocktank with default and custom receive capacity',
async () => {
await receiveOnchainFunds({ sats: 1000_000, expectHighBalanceWarning: true });
Expand Down Expand Up @@ -410,7 +411,8 @@ describe('@transfer - Transfer', () => {
}
);

ciIt(
// Blocktank staging no longer creates orders (synonymdev/blocktank-lsp-http#168)
ciIt.skip(
'@transfer_max, @transfer_staging, @staging - Can fund a Blocktank channel at the settled maximum',
async () => {
await receiveOnchainFunds({ sats: 100_000 });
Expand Down