From 0b221b890354e0a6bb0e72a3a4a884010bd0bb3f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 24 Oct 2026 19:29:21 +0200 Subject: [PATCH] test: cover transfer to spending without blocktank channels --- test/helpers/hardware-wallet.ts | 83 +------ test/helpers/lnd.ts | 37 ++- test/specs/hardware-wallet.e2e.ts | 38 ++- test/specs/multiaddress.e2e.ts | 86 +++---- test/specs/settings.e2e.ts | 10 +- test/specs/transfer.e2e.ts | 374 ++---------------------------- 6 files changed, 125 insertions(+), 503 deletions(-) diff --git a/test/helpers/hardware-wallet.ts b/test/helpers/hardware-wallet.ts index 632132b0..80a37f9b 100644 --- a/test/helpers/hardware-wallet.ts +++ b/test/helpers/hardware-wallet.ts @@ -25,15 +25,11 @@ import { tapFirstByAccessibilityIdPrefix, typeText, handleAndroidAlert, - waitForToast, type BalanceCondition, - dismissBackupTimedSheet, - dismissBackgroundPaymentsTimedSheet, - dismissQuickPayIntro, confirmInputOnKeyboard, } from './actions'; import { openHomeWidgets, openSettings } from './navigation'; -import { deposit, getBackend, mineBlocks } from './regtest'; +import { deposit, mineBlocks } from './regtest'; const E2E_ROOT = path.resolve(__dirname, '..', '..'); const ARTIFACTS_DIR = path.join(E2E_ROOT, 'artifacts'); @@ -305,69 +301,12 @@ export async function sendOnchainFromHardwareWallet({ await tap('Close'); } -export async function transferHardwareWalletToSpending({ - amountSats, - waitForSync, -}: { - amountSats: number; - waitForSync?: () => Promise; -}) { +export async function openHardwareTransferUnavailableSheet() { await doNavigationClose(); await tap('ActivityHardware'); await elementById('HardwareWalletScreen').waitForDisplayed(); await tap('HardwareTransferToSpending'); - - const hasSpendingIntro = await elementById('SpendingIntro-button') - .isDisplayed() - .catch(() => false); - if (hasSpendingIntro) { - await tap('SpendingIntro-button'); - await sleep(800); - } - - await elementById('HardwareTransferAmount').waitForDisplayed({ timeout: 60_000 }); - await elementById('HardwareTransferAmountContinue').waitForEnabled({ timeout: 60_000 }); - await sleep(1000); - await enterAmount(amountSats); - await elementById('HardwareTransferAmountContinue').waitForEnabled({ timeout: 60_000 }); - await tap('HardwareTransferAmountContinue'); - await elementById('HardwareTransferSign').waitForDisplayed({ timeout: 120_000 }); - await tap('HardwareTransferOpenTrezorConnect'); - await approveTrezorPromptsUntil([ - 'HardwareTransferSigned', - 'LightningSettingUp', - 'TransferSuccess', - ]); - await waitForHardwareTransferProgress(); - if (getBackend() === 'local') { - // Local backend does not have Blocktank, - // so we don't wait for the transfer success screen. - await tap('TransferSuccess-button'); - await dismissBackupTimedSheet({ triggerTimedSheet: false }); - } else { - await mineBlocks(1); - if (waitForSync) { - await waitForSync(); - } - for (let i = 0; i < 10; i++) { - try { - await elementById('TransferSuccess').waitForDisplayed({ timeout: 5_000 }); - break; - } catch { - console.info('→ Hardware transfer success not found, mining another block...'); - await mineBlocks(1); - if (waitForSync) { - await waitForSync(); - } - } - } - await elementById('TransferSuccess').waitForDisplayed(); - await tap('TransferSuccess-button'); - await dismissSpendingBalanceToastIfShown(); - await dismissBackupTimedSheet({ triggerTimedSheet: false }); - await dismissBackgroundPaymentsTimedSheet({ triggerTimedSheet: true }); - await dismissQuickPayIntro({ triggerTimedSheet: true }); - } + await elementById('ChannelsUnavailableSheet').waitForDisplayed(); } export async function removeHardwareWalletFromSettings(label: string) { @@ -394,14 +333,6 @@ async function approveTrezorPromptsUntil(testIds: string[]) { throw new Error(`Timed out waiting for Trezor approval (${testIds.join(', ')})`); } -async function waitForHardwareTransferProgress() { - await browser.waitUntil(async () => isAnyDisplayed(['LightningSettingUp', 'TransferSuccess']), { - timeout: 60_000, - interval: 500, - timeoutMsg: 'Timed out waiting for hardware transfer progress', - }); -} - function pressTrezorYes() { try { runTrezorEmulatorQuiet(['send-json', JSON.stringify({ type: 'emulator-press-yes' })]); @@ -435,11 +366,3 @@ async function isDisplayed(testId: string): Promise { return false; } } - -async function dismissSpendingBalanceToastIfShown() { - try { - await waitForToast('SpendingBalanceReadyToast', { timeout: 30_000 }); - } catch { - console.info('→ SpendingBalanceReadyToast not shown after hardware transfer.'); - } -} diff --git a/test/helpers/lnd.ts b/test/helpers/lnd.ts index e43555d9..bd8d2edd 100644 --- a/test/helpers/lnd.ts +++ b/test/helpers/lnd.ts @@ -1,15 +1,20 @@ import BitcoinJsonRpc from 'bitcoin-json-rpc'; import { + acknowledgeExternalSuccess, confirmInputOnKeyboard, doNavigationClose, + dragOnElement, elementById, expectTextWithin, sleep, swipeFullScreen, tap, + tryDismissBackgroundPaymentsIfVisible, + tryDismissQuickPayIntroIfVisible, typeText, } from './actions'; -import { LndConfig, lndHost } from './constants'; +import { LndConfig, lndConfig, lndHost } from './constants'; +import { getBitcoinRpc, mineBlocks } from './regtest'; import { openSettings } from './navigation'; import createLndRpc, { LnRpc, WalletUnlockerRpc } from '@radar/lnrpc'; @@ -131,3 +136,33 @@ export async function checkChannelStatus({ size = '100 000' } = {}) { await doNavigationClose(); await sleep(500); } + +/** + * Opens a channel from savings to the local LND node through manual setup, the way the app funds spending + * now that it no longer buys Blocktank channels. Spends the whole savings balance (ExternalAmountMax). + */ +export async function openChannelToLndFromSavings({ + waitForSync, +}: { + waitForSync?: () => Promise; +} = {}) { + const { lnd, lndNodeID } = await setupLND(getBitcoinRpc(), lndConfig); + if (waitForSync) await waitForSync(); + + const ldkNodeId = await getLDKNodeID(); + await connectToLND(lndNodeID, { navigationClose: false }); + await waitForPeerConnection(lnd, ldkNodeId); + + await tap('ExternalAmountMax'); + await tap('ExternalAmountContinue'); + await sleep(500); + await dragOnElement('GRAB', 'right', 0.95); + await sleep(1000); + await acknowledgeExternalSuccess(); + + await mineBlocks(6); + if (waitForSync) await waitForSync(); + await waitForActiveChannel(lnd, ldkNodeId); + await tryDismissBackgroundPaymentsIfVisible(); + await tryDismissQuickPayIntroIfVisible(); +} diff --git a/test/specs/hardware-wallet.e2e.ts b/test/specs/hardware-wallet.e2e.ts index 2bddb11f..db2569d3 100644 --- a/test/specs/hardware-wallet.e2e.ts +++ b/test/specs/hardware-wallet.e2e.ts @@ -1,10 +1,12 @@ import { completeOnboarding, doNavigationClose, + elementById, expectSavingsBalance, expectSpendingBalance, expectTotalBalance, receiveOnchainFunds, + tap, } from '../helpers/actions'; import initElectrum from '../helpers/electrum'; import { @@ -24,9 +26,9 @@ import { sendOnchainFromHardwareWallet, startHardwareWalletFlowFromSuggestion, stopTrezorEmulator, - transferHardwareWalletToSpending, + openHardwareTransferUnavailableSheet, } from '../helpers/hardware-wallet'; -import { ensureLocalFunds, getBackend, getExternalAddress } from '../helpers/regtest'; +import { ensureLocalFunds, getExternalAddress } from '../helpers/regtest'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; @@ -90,26 +92,22 @@ describe('@hardware_wallet @ios_gate - Hardware Wallet', () => { await expectSpendingBalance(0); }); - ciIt('@hardware_wallet_3 - Can transfer hardware wallet funds to spending', async () => { - const fundingSats = 100_000; - const transferSats = 20_000; + ciIt( + '@hardware_wallet_3 - Hardware wallet transfer to spending explains it is unavailable', + async () => { + const fundingSats = 100_000; - await connectHardwareWalletFromSettings(walletLabel); - await fundHardwareWalletAndAcknowledge({ sats: fundingSats }); - await expectHardwareWalletBalance(fundingSats); - await transferHardwareWalletToSpending({ - amountSats: transferSats, - waitForSync: async () => { - await electrum?.waitForSync(); - }, - }); - await doNavigationClose(); - await expectHardwareWalletBalance(fundingSats, { condition: 'lt' }); - if (getBackend() === 'regtest') { - // We only check balance on staging regtest backend, local does not have Blocktank. - await expectSpendingBalance(0, { condition: 'gt', timeout: 60_000 }); + await connectHardwareWalletFromSettings(walletLabel); + await fundHardwareWalletAndAcknowledge({ sats: fundingSats }); + await expectHardwareWalletBalance(fundingSats); + await openHardwareTransferUnavailableSheet(); + await tap('ChannelsUnavailableSheetCancel'); + await elementById('ChannelsUnavailableSheet').waitForDisplayed({ reverse: true }); + await elementById('HardwareWalletScreen').waitForDisplayed(); + await doNavigationClose(); + await expectHardwareWalletBalance(fundingSats); } - }); + ); ciIt('@hardware_wallet_4 - Can send onchain from hardware wallet', async () => { const fundingSats = 100_000; diff --git a/test/specs/multiaddress.e2e.ts b/test/specs/multiaddress.e2e.ts index dd5c828a..9cd14be5 100644 --- a/test/specs/multiaddress.e2e.ts +++ b/test/specs/multiaddress.e2e.ts @@ -19,7 +19,6 @@ import { switchAndFundEachAddressType, swipeFullScreen, tap, - transferSavingsToSpending, transferSpendingToSavings, type addressTypePreference, getSpendingBalance, @@ -41,6 +40,7 @@ import { checkChannelStatus, connectToLND, getLDKNodeID, + openChannelToLndFromSavings, setupLND, waitForPeerConnection, } from '../helpers/lnd'; @@ -71,49 +71,52 @@ describe('@multi_address - Multi address', () => { await electrum?.stop(); }); - ciIt('@multi_address_1 @ios_nightly - Receive to each address type and send max combined', async () => { - const addressTypes: addressTypePreference[] = ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh', 'p2tr']; - const satsPerAddressType = 100_000; - const { totalFundedSats } = await switchAndFundEachAddressType({ - addressTypes, - satsPerAddressType, - waitForSync: async () => { - await electrum?.waitForSync(); - }, - }); - - const totalBalance = await getTotalBalance(); - const savingsBalance = await getSavingsBalance(); - const spendingBalance = await getSpendingBalance(); - await expect(savingsBalance).toEqual(totalFundedSats); - await expect(spendingBalance).toEqual(0); - await expect(totalBalance).toEqual(totalFundedSats); - - const coreAddress = await getExternalAddress(); - await enterAddress(coreAddress); - await tap('AvailableAmount'); - await tap('ContinueAmount'); - await dragOnElement('GRAB', 'right', 0.95); - await handleOver50PercentAlert(); - await elementById('SendSuccess').waitForDisplayed(); - await tap('Close'); - await mineBlocks(1); - await electrum?.waitForSync(); + ciIt( + '@multi_address_1 @ios_nightly - Receive to each address type and send max combined', + async () => { + const addressTypes: addressTypePreference[] = ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh', 'p2tr']; + const satsPerAddressType = 100_000; + const { totalFundedSats } = await switchAndFundEachAddressType({ + addressTypes, + satsPerAddressType, + waitForSync: async () => { + await electrum?.waitForSync(); + }, + }); - await expectTotalBalance(0); - await expectSavingsBalance(0); - await expectSpendingBalance(0); + const totalBalance = await getTotalBalance(); + const savingsBalance = await getSavingsBalance(); + const spendingBalance = await getSpendingBalance(); + await expect(savingsBalance).toEqual(totalFundedSats); + await expect(spendingBalance).toEqual(0); + await expect(totalBalance).toEqual(totalFundedSats); - const totalBalanceAfter = await getTotalBalance(); - const savingsBalanceAfter = await getSavingsBalance(); - const spendingBalanceAfter = await getSpendingBalance(); - await expect(totalBalanceAfter).toEqual(0); - await expect(savingsBalanceAfter).toEqual(0); - await expect(spendingBalanceAfter).toEqual(0); - }); + const coreAddress = await getExternalAddress(); + await enterAddress(coreAddress); + await tap('AvailableAmount'); + await tap('ContinueAmount'); + await dragOnElement('GRAB', 'right', 0.95); + await handleOver50PercentAlert(); + await elementById('SendSuccess').waitForDisplayed(); + await tap('Close'); + await mineBlocks(1); + await electrum?.waitForSync(); + + await expectTotalBalance(0); + await expectSavingsBalance(0); + await expectSpendingBalance(0); + + const totalBalanceAfter = await getTotalBalance(); + const savingsBalanceAfter = await getSavingsBalance(); + const spendingBalanceAfter = await getSpendingBalance(); + await expect(totalBalanceAfter).toEqual(0); + await expect(savingsBalanceAfter).toEqual(0); + await expect(spendingBalanceAfter).toEqual(0); + } + ); ciIt( - '@multi_address_2, @multi_address_staging, @staging - Receive to each address type, transfer all to spending, close channel to taproot', + '@multi_address_2 - Receive to each address type, open a channel with all savings, close channel to taproot', async () => { const addressTypes: addressTypePreference[] = ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh', 'p2tr']; // const addressTypes: addressTypePreference[] = ['p2tr']; @@ -135,13 +138,12 @@ describe('@multi_address - Multi address', () => { await mineBlocks(1); await electrum?.waitForSync(); - await transferSavingsToSpending({ + await openChannelToLndFromSavings({ waitForSync: async () => { await electrum?.waitForSync(); }, }); await expectSpendingBalance(0, { condition: 'gt' }); - await expectSavingsBalance(0); if (driver.isAndroid) { // pull to refresh due to: diff --git a/test/specs/settings.e2e.ts b/test/specs/settings.e2e.ts index a28a3b93..d4c29128 100644 --- a/test/specs/settings.e2e.ts +++ b/test/specs/settings.e2e.ts @@ -458,9 +458,9 @@ describe('@settings @ios_nightly - Settings', () => { await swipeFullScreen('up'); await elementById('SuggestionsWidget').waitForDisplayed(); - // hide lightningTodo suggestion card - await (await elementByIdWithin('Suggestion-lightning', 'SuggestionDismiss')).click(); - await elementById('Suggestion-lightning').waitForDisplayed({ reverse: true }); + // hide the hardware wallet suggestion card + await (await elementByIdWithin('Suggestion-hardware', 'SuggestionDismiss')).click(); + await elementById('Suggestion-hardware').waitForDisplayed({ reverse: true }); // reset suggestions (now in Widgets settings) await openSettings(); @@ -468,9 +468,9 @@ describe('@settings @ios_nightly - Settings', () => { await tap('ResetSuggestions'); await tap('DialogConfirm'); - // lightning should be visible again + // the hardware wallet card should be visible again await sleep(1000); - await elementById('Suggestion-lightning').waitForDisplayed(); + await elementById('Suggestion-hardware').waitForDisplayed(); }); }); diff --git a/test/specs/transfer.e2e.ts b/test/specs/transfer.e2e.ts index 6deaa08c..4559e0f7 100644 --- a/test/specs/transfer.e2e.ts +++ b/test/specs/transfer.e2e.ts @@ -5,7 +5,6 @@ import { receiveOnchainFunds, tap, expectText, - elementByText, elementsById, elementById, multiTap, @@ -15,18 +14,11 @@ import { elementByIdWithin, enterAddress, dismissQuickPayIntro, - doNavigationClose, waitForToast, - waitForToastBestEffort, acknowledgeExternalSuccess, dismissBackgroundPaymentsTimedSheet, expectNoTextWithin, enterAmount, - expectSavingsBalance, - getSpendingBalance, - getAmountUnder, - tryDismissBackgroundPaymentsIfVisible, - tryDismissQuickPayIntroIfVisible, } from '../helpers/actions'; import { checkChannelStatus, @@ -43,147 +35,11 @@ import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; import { openSettings } from '../helpers/navigation'; -async function isDisplayed(testId: string): Promise { - return elementById(testId) - .isDisplayed() - .catch(() => false); -} - -/** Timed sheets can be queued behind one another, so dismiss until Home is clear. */ -async function dismissHomeSheetsIfPresent() { - for (let attempt = 0; attempt < 4; attempt += 1) { - if (await tryDismissBackgroundPaymentsIfVisible()) { - continue; - } - if (await tryDismissQuickPayIntroIfVisible()) { - continue; - } - return; - } -} - -async function openTransferToSpending() { - await browser.waitUntil( - async () => { - await dismissHomeSheetsIfPresent(); - if (await isDisplayed('TransferToSpending')) { - return true; - } - // Only tap from Home. A delayed Background Payments sheet can cover - // ActivitySavings after waitForHome; keep dismissing and retry. - const onHome = - (await isDisplayed('ActivitySavings')) && (await isDisplayed('ActivitySpending')); - if (!onHome) { - return false; - } - try { - await tap('ActivitySavings', { timeout: 5_000 }); - } catch { - return false; - } - return isDisplayed('TransferToSpending'); - }, - { - timeout: 60_000, - interval: 1_000, - timeoutMsg: 'Savings did not become interactive', - } - ); - await tap('TransferToSpending'); - await browser.waitUntil( - async () => { - if (await isDisplayed('SpendingAmountAvailable')) { - return true; - } - if (await isDisplayed('SpendingIntro-button')) { - await tap('SpendingIntro-button'); - } - return false; - }, - { - timeout: 60_000, - interval: 1_000, - timeoutMsg: 'Transfer amount screen did not open after the intro', - } - ); - await elementById('SpendingAmountContinue').waitForEnabled({ timeout: 60_000 }); -} - -/** Tap Continue again only if the amount screen is still visible. */ -async function continueFromAmount() { - await browser.waitUntil( - async () => { - if (await isDisplayed('SpendingConfirmMore')) { - return true; - } - if (await isDisplayed('SpendingAmountContinue')) { - await tap('SpendingAmountContinue'); - } - return false; - }, - { - timeout: 90_000, - interval: 1_000, - timeoutMsg: 'Spending confirmation did not open from the amount screen', - } - ); -} - -async function continueFromAdvanced() { - await expectText('—', { visible: false, timeout: 60_000 }); - await browser.waitUntil( - async () => { - if (await isDisplayed('SpendingConfirmDefault')) { - return true; - } - if (await isDisplayed('SpendingAdvancedContinue')) { - await tap('SpendingAdvancedContinue'); - } - return false; - }, - { - timeout: 90_000, - interval: 1_000, - timeoutMsg: 'Spending confirmation did not open from the advanced screen', - } - ); -} - -async function waitForHome(timeout = 60_000) { - await browser.waitUntil( - async () => { - await dismissHomeSheetsIfPresent(); - return (await isDisplayed('ActivitySavings')) && (await isDisplayed('ActivitySpending')); - }, - { - timeout, - interval: 2_000, - timeoutMsg: 'Wallet Home did not become visible', - } - ); -} - -async function isProcessingPaymentDisplayed(): Promise { - return elementByText('Processing payment', 'exact') - .isDisplayed() - .catch(() => false); -} - /** * Channel detail after a Blocktank buy: "Processing payment" is transient. * Fast settle can skip it by the time Settings → Channels opens; then the * same usable flag as checkChannelStatus (IsUsableYes after swipe up). */ -async function expectProcessingOrUsableChannel() { - if (await isProcessingPaymentDisplayed()) { - console.info('→ Channel still shows Processing payment'); - return; - } - console.info('→ Processing payment not visible; asserting IsUsableYes after swipe'); - await swipeFullScreen('up'); - await elementById('IsUsableYes').waitForDisplayed(); -} - /** * After each Blocktank buy, Home ActivityShort inserts a Transfer row at the top. * Do not open Savings to read Activity-*: a delayed Background Payments sheet can @@ -193,55 +49,6 @@ async function expectProcessingOrUsableChannel() { * Use expectTextWithin (all descendants) — same pattern as @transfer_max. * Android amount minus is a middle TextView; first/last probes miss it. */ -async function expectHomeTransferRows(transferCount: 1 | 2) { - let transferRowIds: readonly string[]; - switch (transferCount) { - case 1: - transferRowIds = ['ActivityShort-0']; - break; - case 2: - transferRowIds = ['ActivityShort-0', 'ActivityShort-1']; - break; - default: { - const _exhaustive: never = transferCount; - throw new Error(`Unexpected transferCount: ${_exhaustive}`); - } - } - - // ActivityShort-0 is often still the prior Received deposit row; poll until - // Transfer (and amount minus) show via all-descendant expectTextWithin — - // same probe @transfer_max uses. Do not waitForDisplayed alone first. - await browser.waitUntil( - async () => { - await dismissHomeSheetsIfPresent(); - for (const rowId of transferRowIds) { - if (!(await elementById(rowId).isDisplayed().catch(() => false))) { - return false; - } - try { - await expectTextWithin(rowId, 'Transfer', { timeout: 1_500 }); - await expectTextWithin(rowId, '-', { timeout: 1_500 }); - } catch { - return false; - } - } - return true; - }, - { - timeout: 60_000, - interval: 1_000, - timeoutMsg: `Home did not show ${transferCount} transfer row(s)`, - } - ); -} - -async function confirmSpendingTransfer() { - await dragOnElement('GRAB', 'right', 0.95); - await elementById('LightningSettingUp').waitForDisplayed({ timeout: 90_000 }); - await tap('TransferSuccess-button'); - await waitForHome(); -} - /** * A successful Blocktank order is not the end of the transfer: the funding * transaction still needs confirmations before the spending balance is usable. @@ -249,100 +56,6 @@ async function confirmSpendingTransfer() { * single-block retries. The balance itself is the definitive end-to-end * assertion because the readiness toast can be missed. */ -async function settleAndExpectSpendingBalance( - expected: number, - waitForSync: () => Promise -) { - let lastBalance = -1; - const waitForExpectedBalance = async (timeout: number): Promise => { - try { - await browser.waitUntil( - async () => { - await dismissHomeSheetsIfPresent(); - lastBalance = await getSpendingBalance().catch(() => -1); - return lastBalance === expected; - }, - { timeout, interval: 2_000 } - ); - return true; - } catch { - return false; - } - }; - - console.info(`→ Waiting for spending balance ${expected}`); - if (await waitForExpectedBalance(30_000)) { - return; - } - - // Blocktank can broadcast a later channel while the first one is already - // usable. Confirm the current chain once, then add at most one block per - // round so a late broadcast is picked up without an uncontrolled mine loop. - const confirmationBatches = [6, 1, 1, 1, 1, 1]; - for (const blocks of confirmationBatches) { - console.info( - `→ Spending is ${lastBalance}; mining ${blocks} block(s) before the next settlement check` - ); - await mineBlocks(blocks); - await waitForSync(); - await waitForToastBestEffort('SpendingBalanceReadyToast', { timeout: 5_000 }); - if (await waitForExpectedBalance(45_000)) { - return; - } - } - - throw new Error( - `Spending balance did not settle to ${expected}; last readable balance was ${lastBalance}` - ); -} - -async function elementContainsAmount(testId: string, expected: number): Promise { - if (driver.isAndroid) { - return (await getAmountUnder(testId).catch(() => -1)) === expected; - } - - const element = elementById(testId); - const candidates = await Promise.all([ - element.getText().catch(() => ''), - element.getAttribute('label').catch(() => ''), - element.getAttribute('value').catch(() => ''), - ]); - const expectedDigits = String(expected); - return candidates.some((candidate) => - String(candidate).replace(/\D/g, '').includes(expectedDigits) - ); -} - -async function openChannelWithTotalSize(expected: number) { - const channelCount = await (await elementsById('Channel')).length; - for (let index = 0; index < channelCount; index += 1) { - const channels = await elementsById('Channel'); - await channels[index].click(); - await elementById('TotalSize').waitForDisplayed(); - if (await elementContainsAmount('TotalSize', expected)) { - return; - } - await tap('NavigationBack'); - await elementById('Channel').waitForDisplayed(); - } - throw new Error(`No channel with total size ${expected} was found`); -} - -async function openAdvancedSettings() { - let lastError: unknown; - for (let attempt = 1; attempt <= 3; attempt += 1) { - await dismissHomeSheetsIfPresent(); - try { - await openSettings('advanced'); - return; - } catch (error) { - lastError = error; - console.info(`→ Settings drawer did not open (attempt ${attempt})`); - } - } - throw lastError; -} - describe('@transfer - Transfer', () => { let electrum: { waitForSync: () => any; stop: () => void }; @@ -362,77 +75,28 @@ describe('@transfer - Transfer', () => { }); ciIt( - '@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 }); - - // First channel: a 200k spending balance with the default receive capacity. - await openTransferToSpending(); - await enterAmount(200000); - await expectText('200 000', { strategy: 'contains' }); - await continueFromAmount(); - await tap('SpendingConfirmMore'); - await expectText('200 000', { strategy: 'contains' }); - await tap('LiquidityContinue'); - await confirmSpendingTransfer(); - await expectHomeTransferRows(1); - - await settleAndExpectSpendingBalance(200000, async () => electrum?.waitForSync()); - - // Second channel: a 100k spending balance with 150k custom receive capacity. - await openTransferToSpending(); - await enterAmount(100000); - await expectText('100 000', { strategy: 'contains' }); - await continueFromAmount(); - await tap('SpendingConfirmAdvanced'); - await elementById('SpendingAdvancedNumberField').waitForDisplayed({ timeout: 60_000 }); - await enterAmount(150000); - await continueFromAdvanced(); - await expectTextWithin('SpendingConfirmChannel', '100 000'); - await expectTextWithin('SpendingConfirmChannel', '150 000'); - await confirmSpendingTransfer(); - await expectHomeTransferRows(2); - - // Both channel funding transactions must settle into usable spending balance. - await settleAndExpectSpendingBalance(300000, async () => electrum?.waitForSync()); - - // Find the custom channel by value; channel ordering differs by platform. - await openAdvancedSettings(); - await tap('Channels'); - await elementById('Channel').waitForDisplayed(); - await openChannelWithTotalSize(250000); - await expect(await elementContainsAmount('TotalSize', 250000)).toBe(true); - await expectProcessingOrUsableChannel(); - await doNavigationClose(); - - // Home still shows both completed transfers after the channel inspection. - await expectHomeTransferRows(2); - } - ); - - ciIt( - '@transfer_max, @transfer_staging, @staging - Can fund a Blocktank channel at the settled maximum', + '@transfer_1, @transfer_staging, @staging - Transfer to spending explains that automatic transfers are unavailable', async () => { await receiveOnchainFunds({ sats: 100_000 }); - await openTransferToSpending(); - await elementById('SpendingAmountMax').waitForEnabled(); - await tap('SpendingAmountMax'); - const expectedSpendingBalance = await getAmountUnder('SpendingAmountNumberField'); - await expect(expectedSpendingBalance).toBeGreaterThan(0); - await continueFromAmount(); - await dragOnElement('GRAB', 'right', 0.95); - await elementById('LightningSettingUp').waitForDisplayed({ timeout: 90_000 }); - await tap('TransferSuccess-button'); - await waitForHome(); - await settleAndExpectSpendingBalance(expectedSpendingBalance, async () => - electrum?.waitForSync() - ); - await expectSavingsBalance(0); - await elementById('ActivityShort-0').waitForDisplayed(); - await elementById('ActivityShort-1').waitForDisplayed(); - await expectTextWithin('ActivityShort-0', 'Transfer'); - await expectTextWithin('ActivityShort-1', 'Received'); + // Blocktank channel purchases are gone: the button opens the Transfer Unavailable sheet + await tap('ActivitySavings'); + await tap('TransferToSpending'); + await elementById('ChannelsUnavailableSheet').waitForDisplayed(); + await elementById('ChannelsUnavailableSheetTitle').waitForDisplayed(); + await tap('ChannelsUnavailableSheetCancel'); + await elementById('ChannelsUnavailableSheet').waitForDisplayed({ reverse: true }); + + // Continue leads to the third-party provider list and the manual channel setup + await tap('TransferToSpending'); + await tap('ChannelsUnavailableSheetContinue'); + await elementById('ChooseLspScan').waitForDisplayed(); + const providers = await elementsById('ChooseLspProvider'); + await expect(providers).toBeElementsArrayOfSize(4); + await tap('ChooseLspManual'); + await elementById('NodeIdInput').waitForDisplayed(); + await elementById('HostInput').waitForDisplayed(); + await elementById('PortInput').waitForDisplayed(); } );