diff --git a/docs/lightning-primer-for-qa.md b/docs/lightning-primer-for-qa.md index 6d4a54c..12ffaff 100644 --- a/docs/lightning-primer-for-qa.md +++ b/docs/lightning-primer-for-qa.md @@ -34,13 +34,13 @@ The **fix path** uses **`accept_stale_channel_monitors`** so ldk-node can align ## What to test when Lightning / LDK storage changes -| Area | Why | -|------|-----| -| **Cold start** | Any path that reads/writes ChannelManager, monitors, or VSS must not pair **new** manager with **old** monitor. | -| **Backup / restore** | Restoring must be **consistent snapshots**; partial or older monitor alone is high risk. | -| **Migration** | RN → native or schema changes: avoid overwriting live data with **stale** remote copies. | -| **Recovery** | After `DangerousValue` / `accept_stale`: peers reconnect, chain sync completes, **inbound and outbound** payments work, **second launch** does not repeat recovery forever. | -| **Infra noise** | On regtest, **stale RGS** / gossip can cause transient **“route not found”** — distinguish from persistence bugs (see logs for `DangerousValue` vs routing errors). | +| Area | Why | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Cold start** | Any path that reads/writes ChannelManager, monitors, or VSS must not pair **new** manager with **old** monitor. | +| **Backup / restore** | Restoring must be **consistent snapshots**; partial or older monitor alone is high risk. | +| **Migration** | RN → native or schema changes: avoid overwriting live data with **stale** remote copies. | +| **Recovery** | After `DangerousValue` / `accept_stale`: peers reconnect, chain sync completes, **inbound and outbound** payments work, **second launch** does not repeat recovery forever. | +| **Infra noise** | On regtest, **stale RGS** / gossip can cause transient **“route not found”** — distinguish from persistence bugs (see logs for `DangerousValue` vs routing errors). | ## Risks of incorrect “fixes” @@ -50,13 +50,13 @@ The **fix path** uses **`accept_stale_channel_monitors`** so ldk-node can align ## Glossary -| Term | Meaning | -|------|--------| -| **Commitment update** | New off-chain state (balances + HTLC set). | -| **`update_id`** | LDK’s persisted notion of how far the ChannelMonitor has advanced vs the ChannelManager for that channel. | -| **HTLC** | **Hash Time-Locked Contract** — conditional payment inside a commitment (hash lock + time lock). | -| **ChannelMonitor** | Per-channel persisted state for chain watching and dispute handling. | -| **DangerousValue** | LDK/ldk-node refusing to load because continuing would violate safety assumptions (e.g. stale monitor). | +| Term | Meaning | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **Commitment update** | New off-chain state (balances + HTLC set). | +| **`update_id`** | LDK’s persisted notion of how far the ChannelMonitor has advanced vs the ChannelManager for that channel. | +| **HTLC** | **Hash Time-Locked Contract** — conditional payment inside a commitment (hash lock + time lock). | +| **ChannelMonitor** | Per-channel persisted state for chain watching and dispute handling. | +| **DangerousValue** | LDK/ldk-node refusing to load because continuing would violate safety assumptions (e.g. stale monitor). | | **accept_stale_channel_monitors** | Explicit recovery mode to load despite mismatch, then heal via protocol + sync (use only in controlled recovery). | ## See also diff --git a/docs/repro-channel-monitor-desync.md b/docs/repro-channel-monitor-desync.md index 0490601..09b5c9a 100644 --- a/docs/repro-channel-monitor-desync.md +++ b/docs/repro-channel-monitor-desync.md @@ -3,10 +3,12 @@ See also: [Lightning primer for QA](./lightning-primer-for-qa.md) (monitors, HTLCs, gaps, risks). Related issues: + - [#847 (bitkit-android)](https://github.com/synonymdev/bitkit-android/issues/847) - iOS support ticket (user logs from 2026-03-18) Fix branches: + - **iOS**: `fix/stale-monitor-recovery-release` - **Android**: `fix/stale-monitor-recovery-v2` @@ -17,6 +19,7 @@ Build 182 (v2.1.0) introduced `fetchOrphanedChannelMonitorsIfNeeded` which fetch ## Root Cause On v2.1.0 startup: + 1. `fetchOrphanedChannelMonitorsIfNeeded` fetches stale channel monitor from RN backup server 2. Injects it via `setChannelDataMigration` with `channelManager: nil` (monitors only) 3. ldk-node persists the stale monitor to VSS/local storage @@ -33,6 +36,7 @@ Failed to read channel manager from store: Value would be dangerous to continue ``` In app logs: + ``` Running pre-startup channel monitor recovery check Found 1 monitors on RN backup for pre-startup recovery @@ -89,6 +93,7 @@ RN v1.1.6 local builds use `.env.test.template` (regtest + localhost Electrum). **Critical**: The RN app's `.env.production` must point the backup server to **staging** (not localhost), because the native apps have `rnBackupServerHost` hardcoded to staging. If the RN app pushes to `127.0.0.1:3003` but the native app queries `bitkit.stag0.blocktank.to`, it will never find the channel monitors and the bug won't trigger. In `.env.production` for the RN v1.1.6 build, set: + ``` BACKUPS_SERVER_HOST=https://bitkit.stag0.blocktank.to/backups-ldk BACKUPS_SERVER_PUBKEY=02c03b8b8c1b5500b622646867d99bf91676fac0f38e2182c91a9ff0d053a21d6d @@ -130,6 +135,7 @@ The app's channel has all balance on the app side. LND needs outbound liquidity ``` Verify with a test payment: + ```bash ./bitcoin-cli payinvoice 10 ``` @@ -166,6 +172,7 @@ Install v2.1.0 **over** the native app → app fails to start LN node (see error Upgrading from a broken v2.1.0 wallet to v2.1.2 (fix candidate) recovers the wallet. Channels are healed and LN transactions work after recovery. Fix branches: + - **iOS**: `fix/stale-monitor-recovery-release` - **Android**: `fix/stale-monitor-recovery-v2` @@ -192,34 +199,34 @@ Matrix of upgrade/recovery scenarios to validate v2.1.2. Each scenario should be ### Blocktank channel (staging regtest) -| # | Scenario | Result | -|---|----------|--------| -| B1 | v2.0.6 (wallet with 21+ payment gap) → v2.1.0 → confirm broken | Reproduces | -| B2 | Restore broken v2.1.0 wallet into v2.1.2 (clean install + restore) | ✅ Recovered | -| B3 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered | -| B4 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues | -| B5 | v2.0.6 (wallet with gap) → v2.1.1 → v2.1.2 | ✅ No issues | -| B6 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues | -| B7 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered | +| # | Scenario | Result | +| --- | -------------------------------------------------------------------- | ------------ | +| B1 | v2.0.6 (wallet with 21+ payment gap) → v2.1.0 → confirm broken | Reproduces | +| B2 | Restore broken v2.1.0 wallet into v2.1.2 (clean install + restore) | ✅ Recovered | +| B3 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered | +| B4 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues | +| B5 | v2.0.6 (wallet with gap) → v2.1.1 → v2.1.2 | ✅ No issues | +| B6 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues | +| B7 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered | ### 3rd-party channel (local docker) -| # | Scenario | Result | -|---|----------|--------| -| T1 | v2.0.6 (wallet with 30+ payment gap) → v2.1.0 → confirm broken | Reproduces | -| T2 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered | -| T3 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues | -| T4 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues | -| T5 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered | +| # | Scenario | Result | +| --- | -------------------------------------------------------------------- | ------------ | +| T1 | v2.0.6 (wallet with 30+ payment gap) → v2.1.0 → confirm broken | Reproduces | +| T2 | Update broken v2.1.0 wallet to v2.1.2 (in-place upgrade) | ✅ Recovered | +| T3 | v2.0.6 (wallet with gap) → v2.1.2 (skip v2.1.0) | ✅ No issues | +| T4 | v2.1.0 healthy wallet (no gap) → v2.1.2 (regression check) | ✅ No issues | +| T5 | v2.1.0 broken wallet + 600 blocks mined → v2.1.2 (stale chain state) | ✅ Recovered | ### Version reference -| Version | iOS branch | Android branch | -|---------|-----------|---------------| -| v1.1.6 | tag `v1.1.6` (RN) | tag `v1.1.6` (RN) | -| v2.0.6 | `chore/e2e-updater-url` | — | -| v2.0.3 | — | `chore/e2e-updater-url` | -| v2.1.0 | build 182 | build 182 | +| Version | iOS branch | Android branch | +| ------------ | ------------------------------------ | ------------------------------- | +| v1.1.6 | tag `v1.1.6` (RN) | tag `v1.1.6` (RN) | +| v2.0.6 | `chore/e2e-updater-url` | — | +| v2.0.3 | — | `chore/e2e-updater-url` | +| v2.1.0 | build 182 | build 182 | | v2.1.2 (fix) | `fix/stale-monitor-recovery-release` | `fix/stale-monitor-recovery-v2` | --- @@ -233,11 +240,11 @@ Matrix of upgrade/recovery scenarios to validate v2.1.2. Each scenario should be ## Files -| File | Purpose | -|------|---------| -| `docs/lightning-primer-for-qa.md` | Background: ChannelManager vs ChannelMonitor, HTLCs, gaps, test focus | -| `test/specs/receive-ln-payments.e2e.ts` | Automated spec to receive N Lightning payments | -| `wdio.no-install.conf.ts` | WDIO config that attaches to existing app (no reinstall) | -| `docker/bitcoin-cli` | Local docker CLI with `openchannel`, `payinvoice`, `mine`, `send` commands | -| `scripts/pay-lightning-address.sh` | Shell script to pay BOLT11/BIP21/LN address via Blocktank | -| `scripts/pay-lightning-address-loop.sh` | Shell script to send N payments to a Lightning address | +| File | Purpose | +| --------------------------------------- | -------------------------------------------------------------------------- | +| `docs/lightning-primer-for-qa.md` | Background: ChannelManager vs ChannelMonitor, HTLCs, gaps, test focus | +| `test/specs/receive-ln-payments.e2e.ts` | Automated spec to receive N Lightning payments | +| `wdio.no-install.conf.ts` | WDIO config that attaches to existing app (no reinstall) | +| `docker/bitcoin-cli` | Local docker CLI with `openchannel`, `payinvoice`, `mine`, `send` commands | +| `scripts/pay-lightning-address.sh` | Shell script to pay BOLT11/BIP21/LN address via Blocktank | +| `scripts/pay-lightning-address-loop.sh` | Shell script to send N payments to a Lightning address | diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 0b5b1ab..e7fa62e 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -1,6 +1,9 @@ import type { ChainablePromiseElement } from 'webdriverio'; import { reinstallApp } from './setup'; import { deposit, mineBlocks } from './regtest'; +import { doNavigationClose, doTriggerTimedSheet, openSettings } from './navigation'; + +export { doNavigationClose, doTriggerTimedSheet } from './navigation'; export const sleep = (ms: number) => browser.pause(ms); @@ -603,16 +606,8 @@ export async function handleAndroidAlert( } } -export async function doNavigationClose() { - await tap('HeaderMenu'); - await tap('DrawerWallet'); - await sleep(500); -} - export async function getSeed(): Promise { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('BackupSettings'); + await openSettings('security'); await tap('BackupWallet'); // get seed from SeedContainer @@ -810,9 +805,7 @@ async function assertAddressTypeSwitchFeedback() { } export async function switchPrimaryAddressType(nextType: addressTypePreference) { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('AddressTypePreference'); await tap(nextType); await assertAddressTypeSwitchFeedback(); @@ -1165,6 +1158,8 @@ export type ToastId = | 'TransactionRemovedToast' | 'InvalidAddressToast' | 'ExpiredLightningToast' + | 'DevModeEnabledToast' + | 'DevModeDisabledToast' | 'InsufficientSpendingToast' | 'InsufficientSavingsToast'; @@ -1204,23 +1199,6 @@ export async function acknowledgeExternalSuccess() { await sleep(300); } -/** - * Triggers the timed backup sheet by navigating to settings and back. - * Since timed sheets are sometimes triggered by user behavior (when user goes back to home screen), - * we need to trigger them manually. - * - * @example - * // Trigger backup sheet before testing dismissal - * await doTriggerTimedSheet(); - */ -export async function doTriggerTimedSheet() { - await sleep(700); // wait for any previous animations to finish - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await sleep(500); // wait for the app to settle - await doNavigationClose(); -} - export async function dismissBackgroundPaymentsTimedSheet({ triggerTimedSheet = false, }: { triggerTimedSheet?: boolean } = {}) { @@ -1363,9 +1341,7 @@ export async function acknowledgeHighBalanceWarning({ // enable/disable widgets in settings export async function toggleWidgets() { await sleep(3000); - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('WidgetsSettings'); const widgets = await elementsByText('Widgets'); await widgets[1].click(); @@ -1476,8 +1452,7 @@ export async function attemptRefreshOnHomeScreen() { } export async function waitForBackup() { - await tap('HeaderMenu'); - await tap('DrawerSettings'); + await openSettings('security'); await tap('BackupSettings'); await elementById('AllSynced').waitForDisplayed(); await doNavigationClose(); diff --git a/test/helpers/lnd.ts b/test/helpers/lnd.ts index 3ceee48..d41c195 100644 --- a/test/helpers/lnd.ts +++ b/test/helpers/lnd.ts @@ -10,6 +10,7 @@ import { typeText, } from './actions'; import { LndConfig } from './constants'; +import { openSettings } from './navigation'; import createLndRpc, { LnRpc, WalletUnlockerRpc } from '@radar/lnrpc'; export async function setupLND( @@ -94,9 +95,7 @@ export async function waitForActiveChannel( } export async function getLDKNodeID(): Promise { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); // wait for LDK to start await sleep(5000); await tap('LightningNodeInfo'); @@ -123,9 +122,7 @@ export async function connectToLND(lndNodeID: string, { navigationClose = true } } export async function checkChannelStatus({ size = '100 000' } = {}) { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('Channels'); await sleep(1000); await tap('Channel'); diff --git a/test/helpers/navigation.ts b/test/helpers/navigation.ts new file mode 100644 index 0000000..9e232bf --- /dev/null +++ b/test/helpers/navigation.ts @@ -0,0 +1,46 @@ +import { tap, sleep } from './actions'; + +export type SettingsTab = 'general' | 'security' | 'advanced'; + +/** + * Opens the Settings screen at the given tab. + * General is the default tab so no extra tap is needed for it. + */ +export async function openSettings(tab: SettingsTab = 'general') { + await tap('HeaderMenu'); + await tap('DrawerSettings'); + if (tab !== 'general') { + await tap(`Tab-${tab}`); + await sleep(300); + } +} + +/** + * Opens the Support screen from the drawer menu. + */ +export async function openSupport() { + await tap('HeaderMenu'); + await tap('DrawerSupport'); +} + +/** + * Closes the drawer and navigates back to the Wallet home screen. + */ +export async function doNavigationClose() { + await tap('HeaderMenu'); + await tap('DrawerWallet'); + await sleep(500); +} + +/** + * Triggers the timed backup sheet by navigating to settings and back. + * Since timed sheets are sometimes triggered by user behavior (when user goes back to home screen), + * we need to trigger them manually. + */ +export async function doTriggerTimedSheet() { + await sleep(700); + await tap('HeaderMenu'); + await tap('DrawerSettings'); + await sleep(500); + await doNavigationClose(); +} diff --git a/test/helpers/regtest.ts b/test/helpers/regtest.ts index 0e9bc5b..dfdc4f6 100644 --- a/test/helpers/regtest.ts +++ b/test/helpers/regtest.ts @@ -51,7 +51,7 @@ async function localMineBlocks(count: number): Promise { function lndRestRequest( path: string, - body: Record, + body: Record ): Promise> { const tlsCert = fs.readFileSync(lndConfig.tls); const macaroon = fs.readFileSync(lndConfig.macaroonPath).toString('hex'); @@ -74,7 +74,9 @@ function lndRestRequest( }, (res) => { let data = ''; - res.on('data', (chunk: Buffer) => { data += chunk.toString(); }); + res.on('data', (chunk: Buffer) => { + data += chunk.toString(); + }); res.on('end', () => { if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { resolve(JSON.parse(data) as Record); @@ -82,7 +84,7 @@ function lndRestRequest( reject(new Error(`LND REST ${res.statusCode}: ${data}`)); } }); - }, + } ); req.on('error', reject); req.write(payload); diff --git a/test/specs/backup.e2e.ts b/test/specs/backup.e2e.ts index e78aec2..c96e2f6 100644 --- a/test/specs/backup.e2e.ts +++ b/test/specs/backup.e2e.ts @@ -19,6 +19,7 @@ import { } from '../helpers/actions'; import { ciIt } from '../helpers/suite'; import { ensureLocalFunds } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@backup - Backup', () => { let electrum: Awaited> | undefined; @@ -66,9 +67,7 @@ describe('@backup - Backup', () => { await tap('NavigationBack'); // - change settings (currency to GBP) // - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('CurrenciesSettings'); const gbp_opt = await elementByText('GBP (£)'); await gbp_opt.waitForDisplayed(); diff --git a/test/specs/lightning.e2e.ts b/test/specs/lightning.e2e.ts index fc58e7c..2b62b4d 100644 --- a/test/specs/lightning.e2e.ts +++ b/test/specs/lightning.e2e.ts @@ -39,6 +39,7 @@ import { } from '../helpers/lnd'; import { ciIt } from '../helpers/suite'; import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@lightning - Lightning', () => { let electrum: { waitForSync: any; stop: any }; @@ -262,9 +263,7 @@ describe('@lightning - Lightning', () => { await doNavigationClose(); // check channel status - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('Channels'); await sleep(2000); await tap('Channel'); diff --git a/test/specs/multiaddress.e2e.ts b/test/specs/multiaddress.e2e.ts index 410c9c4..fdd1563 100644 --- a/test/specs/multiaddress.e2e.ts +++ b/test/specs/multiaddress.e2e.ts @@ -51,6 +51,7 @@ import { getExternalAddress, mineBlocks, } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@multi_address - Multi address', () => { let electrum: Awaited> | undefined; @@ -163,10 +164,7 @@ describe('@multi_address - Multi address', () => { await swipeFullScreen('down'); // check in address viewer all savings are in taproot address - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await sleep(1000); - await tap('AdvancedSettings'); + await openSettings('advanced'); await sleep(1000); await tap('AddressViewer'); await sleep(1000); @@ -238,10 +236,7 @@ describe('@multi_address - Multi address', () => { await expect(remainingTotal).toBeGreaterThan(0); // verify change is in taproot address - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await sleep(1000); - await tap('AdvancedSettings'); + await openSettings('advanced'); await sleep(1000); await tap('AddressViewer'); await sleep(1000); @@ -294,10 +289,7 @@ describe('@multi_address - Multi address', () => { const savingsBalance = await getSavingsBalance(); await expect(savingsBalance).toEqual(satsPerAddressType); - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await sleep(1000); - await tap('AdvancedSettings'); + await openSettings('advanced'); await sleep(1000); await tap('AddressViewer'); await sleep(1000); diff --git a/test/specs/numberpad.e2e.ts b/test/specs/numberpad.e2e.ts index 5e3094d..9f548ca 100644 --- a/test/specs/numberpad.e2e.ts +++ b/test/specs/numberpad.e2e.ts @@ -15,6 +15,7 @@ import initElectrum from '../helpers/electrum'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; import { ensureLocalFunds } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@numberpad - NumberPad', () => { let electrum: Awaited> | undefined; @@ -178,9 +179,7 @@ async function makeSureIsBitcoinInput(mode: NumberpadMode) { } async function switchToClassicDenomination() { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('UnitSettings'); await tap('DenominationClassic'); await doNavigationClose(); diff --git a/test/specs/onboarding.e2e.ts b/test/specs/onboarding.e2e.ts index 934777c..614b9f8 100644 --- a/test/specs/onboarding.e2e.ts +++ b/test/specs/onboarding.e2e.ts @@ -14,6 +14,7 @@ import { } from '../helpers/actions'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { openSettings } from '../helpers/navigation'; describe('@onboarding - Onboarding', () => { beforeEach(async () => { @@ -85,9 +86,7 @@ describe('@onboarding - Onboarding', () => { // Go to Address Viewer await swipeFullScreen('down'); - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('AddressViewer'); const address0Element = await elementById('Address-0'); diff --git a/test/specs/onchain.e2e.ts b/test/specs/onchain.e2e.ts index 71a30de..6eec699 100644 --- a/test/specs/onchain.e2e.ts +++ b/test/specs/onchain.e2e.ts @@ -35,6 +35,7 @@ import { mineBlocks, sendToAddress, } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@onchain - Onchain', () => { let electrum: Awaited> | undefined; @@ -252,9 +253,7 @@ describe('@onchain - Onchain', () => { await receiveOnchainFunds({ sats: 100_000_000, expectHighBalanceWarning: true }); // enable warning for sending over 100$ to test multiple warning dialogs - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('SendAmountWarning'); await doNavigationClose(); diff --git a/test/specs/receive-ln-payments.e2e.ts b/test/specs/receive-ln-payments.e2e.ts index 765ee17..dd40a2a 100644 --- a/test/specs/receive-ln-payments.e2e.ts +++ b/test/specs/receive-ln-payments.e2e.ts @@ -78,7 +78,9 @@ describe('Receive LN payments (utility)', () => { console.error(`${label} ✗ Failed: ${error}`); try { await swipeFullScreen('down'); - } catch { /* ignore */ } + } catch { + /* ignore */ + } await sleep(2000); } } diff --git a/test/specs/security.e2e.ts b/test/specs/security.e2e.ts index 73f1876..bad5a09 100644 --- a/test/specs/security.e2e.ts +++ b/test/specs/security.e2e.ts @@ -15,6 +15,7 @@ import initElectrum from '../helpers/electrum'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; import { ensureLocalFunds, getExternalAddress } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@security - Security And Privacy', () => { let electrum: { waitForSync: any; stop: any }; @@ -47,12 +48,11 @@ describe('@security - Security And Privacy', () => { // - enter wrong PIN 8 times and reset the app // - set up PIN - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('PINCode'); await sleep(1000); - await tap('SecureWalletContinue'); + await tap('EnablePin'); + await sleep(1000); await multiTap('N1', PIN_LENGTH); // enter PIN await multiTap('N2', PIN_LENGTH); // retype wrong PIN await elementById('WrongPIN').waitForDisplayed(); // WrongPIN warning should appear @@ -89,10 +89,9 @@ describe('@security - Security And Privacy', () => { await expect(totalBalance).not.toHaveText('100 000'); // change PIN, restart the app and try it - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); - await tap('PINChange'); + await openSettings('security'); + await tap('PINCode'); + await tap('ChangePIN'); await multiTap('N3', PIN_LENGTH); await elementById('AttemptsRemaining').waitForDisplayed(); await sleep(1000); @@ -112,9 +111,7 @@ describe('@security - Security And Privacy', () => { await elementById('TotalBalance').waitForDisplayed(); // disable PIN, restart the app, it should not ask for it - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('PINCode'); await tap('DisablePin'); await multiTap('N2', PIN_LENGTH); @@ -123,12 +120,11 @@ describe('@security - Security And Privacy', () => { await elementById('TotalBalance').waitForDisplayed(); // enable PIN for last test - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('PINCode'); await sleep(1000); - await tap('SecureWalletContinue'); + await tap('EnablePin'); + await sleep(1000); await multiTap('N1', PIN_LENGTH); // enter PIN await multiTap('N1', PIN_LENGTH); // retype PIN await tap('SkipButton'); // skip Biometrics for now @@ -151,6 +147,7 @@ describe('@security - Security And Privacy', () => { await multiTap('N7', PIN_LENGTH); // wrong PIN on the last attempt await sleep(1000); // app should reset itself and show onboarding + await expectText('Privacy Policy'); await elementById('Continue').waitForDisplayed(); }); }); diff --git a/test/specs/send.e2e.ts b/test/specs/send.e2e.ts index fa9c9a5..cbd4a20 100644 --- a/test/specs/send.e2e.ts +++ b/test/specs/send.e2e.ts @@ -46,6 +46,7 @@ import { getExternalAddress, mineBlocks, } from '../helpers/regtest'; +import { openSettings } from '../helpers/navigation'; describe('@send - Send', () => { let electrum: { waitForSync: any; stop: any }; @@ -471,9 +472,7 @@ describe('@send - Send', () => { // enable quickpay console.info('Enabling quickpay...'); - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('QuickpaySettings'); // no quickpay intro as we already dismissed it after getting lightning balance await tap('QuickpayToggle'); diff --git a/test/specs/settings.e2e.ts b/test/specs/settings.e2e.ts index 3a37bff..abd5911 100644 --- a/test/specs/settings.e2e.ts +++ b/test/specs/settings.e2e.ts @@ -21,6 +21,7 @@ import { import { electrumHost, electrumPort } from '../helpers/constants'; import { launchFreshApp, reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { openSettings, openSupport } from '../helpers/navigation'; describe('@settings - Settings', () => { before(async () => { @@ -48,9 +49,7 @@ describe('@settings - Settings', () => { } // - change settings (currency to EUR) // - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('CurrenciesSettings'); const eur_opt = await elementByText('EUR (€)'); await eur_opt.waitForDisplayed(); @@ -65,9 +64,7 @@ describe('@settings - Settings', () => { await expect(fiatSymbol).toHaveText('₿'); // switch to USD - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('CurrenciesSettings'); const usd_opt = await elementByText('USD ($)'); await usd_opt.waitForDisplayed(); @@ -79,9 +76,7 @@ describe('@settings - Settings', () => { const fiatSymbol = await elementByIdWithin('TotalBalance-primary', 'MoneyFiatSymbol'); const balance = await elementByIdWithin('TotalBalance-primary', 'MoneyText'); - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); // switch to USD await tap('UnitSettings'); @@ -94,9 +89,7 @@ describe('@settings - Settings', () => { await expect(balance).toHaveText('0.00'); // switch back to BTC - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('UnitSettings'); await tap('Bitcoin'); await tap('NavigationBack'); @@ -105,10 +98,7 @@ describe('@settings - Settings', () => { await expect(balance).toHaveText('0'); // switch to classic denomination - await tap('HeaderMenu'); - await sleep(500); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('UnitSettings'); await tap('DenominationClassic'); await tap('NavigationBack'); @@ -118,9 +108,7 @@ describe('@settings - Settings', () => { }); ciIt('@settings_03 - Can switch transaction speed', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); // switch to Fast await tap('TransactionSpeedSettings'); @@ -152,9 +140,7 @@ describe('@settings - Settings', () => { ciIt('@settings_04 - Can remove last used tags', async () => { // no tags, menu entry should be hidden - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await elementById('TagsSettings').waitForDisplayed({ reverse: true }); await doNavigationClose(); @@ -175,12 +161,11 @@ describe('@settings - Settings', () => { await sleep(1000); // wait for the app to settle // open tag manager, delete tag - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('TagsSettings'); (await elementByText(tag)).waitForDisplayed(); await tap(`Tag-${tag}-delete`); + await tap('NavigationBack'); await doNavigationClose(); // open receive tags, check tags are gone @@ -192,10 +177,8 @@ describe('@settings - Settings', () => { (await elementByText(tag)).waitForDisplayed({ reverse: true }); }); - ciIt('@settings_05 - Can show About screen', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('About'); + ciIt('@settings_05 - Can show Support screen', async () => { + await openSupport(); await elementById('AboutLogo').waitForDisplayed(); }); }); @@ -229,9 +212,7 @@ describe('@settings - Settings', () => { } // Disable 'swipe to hide balance' - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('SwipeBalanceToHide'); await doNavigationClose(); @@ -243,9 +224,7 @@ describe('@settings - Settings', () => { await elementById('ShowBalance').waitForDisplayed({ reverse: true }); // Enable 'hide balance on open' - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('SecuritySettings'); + await openSettings('security'); await tap('SwipeBalanceToHide'); await tap('HideBalanceOnOpen'); @@ -260,11 +239,9 @@ describe('@settings - Settings', () => { }); }); - describe('Backup or restore', () => { + describe('Security - Backup or reset', () => { ciIt('@settings_07 - Can show backup and validate it', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('BackupSettings'); + await openSettings('security'); await sleep(1000); await tap('ResetAndRestore'); await sleep(1000); @@ -315,9 +292,7 @@ describe('@settings - Settings', () => { await sleep(1000); // switch to Legacy - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('AddressTypePreference'); await tap('p2pkh'); await sleep(1000); // We need a second after switching address types. @@ -332,9 +307,7 @@ describe('@settings - Settings', () => { await sleep(1000); // switch back to Native segwit - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('AddressTypePreference'); await tap('p2wpkh'); await doNavigationClose(); @@ -342,35 +315,15 @@ describe('@settings - Settings', () => { }); ciIt('@settings_09 - Can open LN settings screens', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - // LDKDebug, CopyNodeId, RefreshLDK, RestartLDK and RebroadcastLDKTXS N/A in DevSettings - // for (let i = 1; i <= 5; i++) { - // await tap('DevOptions'); - // } - // await tap('DevSettings'); - // await tap('LDKDebug'); - // await tap('CopyNodeId'); - // await tap('RefreshLDK'); - // await tap('RestartLDK'); - // await tap('RebroadcastLDKTXS'); - // await tap('NavigationBack'); - // await tap('NavigationBack'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('LightningNodeInfo'); await elementById('LDKNodeID').waitForDisplayed(); await tap('NavigationBack'); - await tap('NavigationBack'); - // for (let i = 1; i <= 5; i++) { - // await tap('DevOptions'); - // } await sleep(1000); }); ciIt('@settings_10 - Can enter wrong Electrum server and get an error message', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('ElectrumConfig'); // enter wrong electrum server address @@ -461,9 +414,7 @@ describe('@settings - Settings', () => { }); ciIt('@settings_11 - Can connect to different Rapid Gossip Sync Server', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('RGSServer'); await sleep(1000); @@ -498,44 +449,48 @@ describe('@settings - Settings', () => { await (await elementByIdWithin('Suggestion-lightning', 'SuggestionDismiss')).click(); await elementById('Suggestion-lightning').waitForDisplayed({ reverse: true }); - // reset suggestions - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + // reset suggestions (now in Widgets settings) + await openSettings(); + await tap('WidgetsSettings'); await tap('ResetSuggestions'); await tap('DialogConfirm'); // lightning should be visible again await sleep(1000); - // if (driver.isIOS) { - // await swipeFullScreen('up'); - // } await elementById('Suggestion-lightning').waitForDisplayed(); }); }); describe('Dev Settings', () => { ciIt('@settings_13 - Can show/hide Dev Settings', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); + // verify DevSettings is visible in Advanced tab + await openSettings('advanced'); await elementById('DevSettings').waitForDisplayed(); await tap('DevSettings'); await sleep(1000); - await tap('NavigationBack'); + // disable dev mode via Support screen (DevOptions = version multi-tap) + await openSupport(); await multiTap('DevOptions', 5); + await waitForToast('DevModeDisabledToast'); + + // DevSettings should be hidden in Advanced tab + await openSettings('advanced'); await elementById('DevSettings').waitForDisplayed({ reverse: true }); + // re-enable dev mode + await openSupport(); await multiTap('DevOptions', 5); + await waitForToast('DevModeEnabledToast'); + + await openSettings('advanced'); await elementById('DevSettings').waitForDisplayed(); }); }); describe('Support', () => { ciIt('@settings_14 - Can see app status', async () => { - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('Support'); + await openSupport(); await tap('AppStatus'); await elementById('Status-internet').waitForDisplayed(); diff --git a/test/specs/transfer.e2e.ts b/test/specs/transfer.e2e.ts index c3fb858..e7c9e6c 100644 --- a/test/specs/transfer.e2e.ts +++ b/test/specs/transfer.e2e.ts @@ -34,8 +34,9 @@ import { import { lndConfig } from '../helpers/constants'; import { ensureLocalFunds, getBitcoinRpc, mineBlocks } from '../helpers/regtest'; -import { launchFreshApp, reinstallApp } from '../helpers/setup'; +import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { openSettings } from '../helpers/navigation'; describe('@transfer - Transfer', () => { let electrum: { waitForSync: () => any; stop: () => void }; @@ -71,9 +72,7 @@ describe('@transfer - Transfer', () => { await receiveOnchainFunds({ sats: 1000_000, expectHighBalanceWarning: true }); // switch to EUR - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('GeneralSettings'); + await openSettings(); await tap('CurrenciesSettings'); await elementByText('EUR (€)').click(); await doNavigationClose(); @@ -236,9 +235,7 @@ describe('@transfer - Transfer', () => { await expectText('TRANSFER IN PROGRESS'); // check channel status - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('Channels'); await sleep(1000); const channels = await elementsById('Channel'); @@ -372,9 +369,7 @@ describe('@transfer - Transfer', () => { await sleep(1000); // check channel is closed - await tap('HeaderMenu'); - await tap('DrawerSettings'); - await tap('AdvancedSettings'); + await openSettings('advanced'); await tap('Channels'); await expectText('Connection 1', { visible: false }); }); diff --git a/test/specs/widgets.e2e.ts b/test/specs/widgets.e2e.ts index 1ad9a95..a1dae2f 100644 --- a/test/specs/widgets.e2e.ts +++ b/test/specs/widgets.e2e.ts @@ -6,7 +6,9 @@ import { swipeFullScreen, completeOnboarding, deleteAllDefaultWidgets, + doNavigationClose, } from '../helpers/actions'; +import { openSettings } from '../helpers/navigation'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; @@ -108,4 +110,66 @@ describe('@widgets - Widgets', () => { await elementByText('Yes, Delete').click(); await elementById('WidgetsAdd').waitForDisplayed(); }); + + ciIt('@widgets_2 - Widget settings: reset, show/hide, titles', async () => { + await deleteAllDefaultWidgets(); + + // Reset widgets via Widget Settings + await openSettings(); + await tap('WidgetsSettings'); + await tap('ResetWidgets'); + await tap('DialogConfirm'); + await sleep(1000); + + // Verify widgets are restored + await swipeFullScreen('up'); + await elementById('PriceWidget').waitForDisplayed(); + await elementById('SuggestionsWidget').waitForDisplayed(); + await elementById('BlocksWidget').waitForDisplayed(); + + // Toggle off "Show Widgets" + await openSettings(); + await tap('WidgetsSettings'); + await tap('ShowWidgets'); + await tap('NavigationBack'); + await doNavigationClose(); + + // Verify widgets are hidden on home + await swipeFullScreen('up'); + await elementById('PriceWidget').waitForDisplayed({ + reverse: true, + timeout: 5000, + }); + await elementById('SuggestionsWidget').waitForDisplayed({ + reverse: true, + timeout: 5000, + }); + await elementById('BlocksWidget').waitForDisplayed({ + reverse: true, + timeout: 5000, + }); + + // Toggle on "Show Widgets" + enable "Show Widget Titles" + await openSettings(); + await tap('WidgetsSettings'); + await tap('ShowWidgets'); + await tap('ShowWidgetTitles'); + await tap('NavigationBack'); + await doNavigationClose(); + + // Verify widgets visible with titles + await swipeFullScreen('up'); + await elementById('PriceWidget').waitForDisplayed(); + await elementById('SuggestionsWidget').waitForDisplayed(); + await elementById('BlocksWidget').waitForDisplayed(); + await elementByText('Bitcoin Price').waitForDisplayed(); + await elementByText('Bitcoin Blocks').waitForDisplayed(); + + // Cleanup: disable widget titles to restore defaults + await openSettings(); + await tap('WidgetsSettings'); + await tap('ShowWidgetTitles'); + await tap('NavigationBack'); + await doNavigationClose(); + }); });