Skip to content

Commit a4aa088

Browse files
committed
yarn+e2e: add Silent Payments test
1 parent af61d89 commit a4aa088

4 files changed

Lines changed: 21942 additions & 21962 deletions

File tree

e2e/silentpayments.spec.mjs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import {test, expect, byModel} from './fixtures.mjs';
2+
3+
// The hermetic tests below cover the settings/keys UI only — scanning
4+
// needs a block-dn server. The full pipeline (header sync, binary tweak
5+
// download, ECDH matching, block identification, spent-ness lookup) is
6+
// covered by the opt-in @live test at the bottom, which scans real signet
7+
// data for a known payment:
8+
//
9+
// E2E_LIVE=1 npx playwright test silentpayments --project chromium-prod
10+
//
11+
// It requires a signet block-dn that serves the binary /sp/tweaks format
12+
// (block-dn v1.4.1+) and re-downloads chain data into a fresh browser
13+
// profile, so expect a few minutes of wall time.
14+
const scanButton = (page) => page.getByRole('button', {name: 'Scan'});
15+
const resultRows = (page) => page.locator('table tbody tr');
16+
17+
// A real signet payment used as a permanent test vector: 100,000 sats to
18+
// the address below, mined in block 313,552.
19+
const LIVE = {
20+
scanPriv:
21+
'd93a9e640712daf16349c137b5b10b60aae1329fe4be304dd438918e0537241d',
22+
spendPub:
23+
'0333e80ffc460ec39f9db07f99a55f393860a19d038e173ef6916935429e80db60',
24+
address:
25+
'tsp1qqw5e8ymuncmhf5hwcmxz96ujhxsqfgnvaywm8zrw3w7nxu6e8vxnuqenaq8lc3' +
26+
'swcw0emvrlnxj47wfcvzse6quwzul0dytfx4pfaqxmvqlahfz2',
27+
fromHeight: '312000',
28+
outpoint:
29+
'd804ace479eab81985d01db7aa0914b0f56479e48dc1358ed5efcdb9b50293f5:0',
30+
height: '313552',
31+
// Deterministic for these keys + transaction; cross-verified through the
32+
// Node engine (tools/sp-demo.mjs) against the same server data.
33+
privKeyTweak:
34+
'b8b36ea2a2a8401d78f0b23de29b403e8f02247b8e7c5d442e992918d636cf9b',
35+
};
36+
37+
test.describe('silentpayments', () => {
38+
test.beforeEach(async ({gotoPage}) => {
39+
await gotoPage('/silentpayments');
40+
});
41+
42+
test('defaults: Signet preselected, dust level 0, empty result table @smoke', async ({page}) => {
43+
const network = byModel(page, 'vm.network');
44+
await expect(network.locator('option').first()).toHaveText('Signet');
45+
await expect(network.locator('option:checked')).toHaveText('Signet');
46+
await expect(byModel(page, 'vm.server'))
47+
.toHaveValue('https://signet.block-dn.org');
48+
await expect(byModel(page, 'vm.batchSize')).toHaveValue('4');
49+
50+
const dust = byModel(page, 'vm.dustLimit');
51+
await expect(dust.locator('option')).toHaveCount(4);
52+
await expect(dust.locator('option:checked'))
53+
.toHaveText('0 sats (complete scan)');
54+
await expect(dust.locator('option').last())
55+
.toHaveText('3750 sats (fastest, economical outputs only)');
56+
57+
await expect(scanButton(page)).toBeEnabled();
58+
await expect(page.getByText('none found yet')).toBeVisible();
59+
});
60+
61+
test('key validation rejects malformed input without scanning', async ({page}) => {
62+
// Empty keys: the scan private key is checked first.
63+
await scanButton(page).click();
64+
await expect(page.locator('.alert-danger'))
65+
.toContainText('scan private key must be 32 bytes');
66+
67+
// Valid scan key, malformed spend key.
68+
await byModel(page, 'vm.scanPrivKey').fill(LIVE.scanPriv);
69+
await byModel(page, 'vm.spendPubKey').fill('02abcd');
70+
await scanButton(page).click();
71+
await expect(page.locator('.alert-danger'))
72+
.toContainText('spend public key must be a 33-byte');
73+
74+
// Validation fails synchronously — no engine was opened, the page
75+
// stays idle.
76+
await expect(scanButton(page)).toBeEnabled();
77+
await expect(page.getByText('none found yet')).toBeVisible();
78+
});
79+
80+
test('network switch auto-fills the server, custom entries persist', async ({page}) => {
81+
const server = byModel(page, 'vm.server');
82+
await byModel(page, 'vm.network').selectOption({label: 'Mainnet'});
83+
await expect(server).toHaveValue('https://block-dn.org');
84+
85+
// A self-hosted server survives switching networks.
86+
await server.fill('http://localhost:8080');
87+
await byModel(page, 'vm.network').selectOption({label: 'Signet'});
88+
await expect(server).toHaveValue('http://localhost:8080');
89+
});
90+
91+
test('real signet scan finds the block-313552 payment @live', async ({page, consoleErrors}) => {
92+
test.skip(!process.env.E2E_LIVE,
93+
'live network test — set E2E_LIVE=1 to run against signet');
94+
// Fresh profile: full signet header + filter-header sync, then a
95+
// ~1,600-block scan over a spam-heavy range.
96+
test.setTimeout(900_000);
97+
98+
await byModel(page, 'vm.scanPrivKey').fill(LIVE.scanPriv);
99+
await byModel(page, 'vm.spendPubKey').fill(LIVE.spendPub);
100+
await byModel(page, 'vm.fromHeight').fill(LIVE.fromHeight);
101+
await byModel(page, 'vm.dustLimit')
102+
.selectOption({label: '0 sats (complete scan)'});
103+
await scanButton(page).click();
104+
105+
// The scan announces itself, derives the address, and streams
106+
// per-range timing lines into the log.
107+
const log = page.locator('pre');
108+
await expect(log).toContainText(
109+
'scanning from height 312000 at dust filter level 0',
110+
{timeout: 120_000});
111+
await expect(page.locator('body')).toContainText(LIVE.address,
112+
{timeout: 300_000});
113+
114+
// The known payment appears as a found output...
115+
const row = resultRows(page).filter({hasText: LIVE.outpoint});
116+
await expect(row).toHaveCount(1, {timeout: 840_000});
117+
await expect(row).toContainText('100,000');
118+
await expect(row).toContainText(LIVE.height);
119+
await expect(row).toContainText('base');
120+
// ...with a resolved spent-ness (not the '?' fallback) and the exact
121+
// private key tweak. Regexes don't get Playwright's whitespace
122+
// normalization, so tolerate the template's cell padding explicitly.
123+
await expect(row.locator('td').nth(5)).toHaveText(/^\s*(yes|spent)\s*$/);
124+
await expect(row.locator('td').last())
125+
.toHaveText(new RegExp(`^\\s*${LIVE.privKeyTweak}\\s*$`));
126+
127+
// The run completes with stats + timing breakdown lines (the stats
128+
// line shows both as the summary paragraph and in the log).
129+
await expect(log).toContainText('outputs found', {timeout: 120_000});
130+
await expect(log).toContainText('ecdh derive');
131+
await expect(page.locator('p').filter({hasText: 'eligible txs scanned'}))
132+
.toBeVisible();
133+
134+
// A live CDN occasionally serves a transient 5xx that the client
135+
// retries transparently; the browser still logs it as a resource
136+
// error. Those are expected here — real failures (page errors,
137+
// application console.error calls) still fail the test.
138+
const transient = (e) => e.startsWith('console.error: Failed to load resource');
139+
for (let i = consoleErrors.length - 1; i >= 0; i--) {
140+
if (transient(consoleErrors[i])) {
141+
consoleErrors.splice(i, 1);
142+
}
143+
}
144+
});
145+
});

0 commit comments

Comments
 (0)