aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-11 22:30:10 -0700
committerAdam Soltys <[email protected]>2026-10-11 22:30:10 -0700
commit8b65caee4348073f44c36d379bc654c27f5e9ee5 (patch)
treed21c03cf877689d39c7fc0be263afe46b751fb1d /tools
parent4f49202c80662e617c9d666928b7e40df37cb9b8 (diff)
downloadcoinosv3-8b65caee4348073f44c36d379bc654c27f5e9ee5.tar.gz
coinosv3-8b65caee4348073f44c36d379bc654c27f5e9ee5.zip
Feed: ⚡ on a locked wallet says so and opens the Accounts page. A locked wallet — the watch-only view of a sealed vault, or a nostr identity signed in over one (Adam's debug Chrome: three watch-only accounts, the vault gone, his extension identity on the first) — has no keys to pay with, and the zap button knew it: it got no handler at all, so the tap pressed the icon and nothing more, under an arrow cursor. Now the tap toasts "Your wallet is locked, so it can't zap. Unlock it to send sats." and opens the Accounts page, where the padlock and Add account are; the open feed is remembered for the way back (by URL, or by id for Following, which has none — returnAfterSignin learns the id form). tools/zap-locked-test.js.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r--tools/zap-locked-test.js59
1 files changed, 59 insertions, 0 deletions
diff --git a/tools/zap-locked-test.js b/tools/zap-locked-test.js
new file mode 100644
index 0000000..3077c1f
--- /dev/null
+++ b/tools/zap-locked-test.js
@@ -0,0 +1,59 @@
+// A locked wallet (the watch-only view of a sealed vault, with an identity
+// signed in over it) can't pay: tapping ⚡ on a post used to do nothing at
+// all — the button pressed, the cursor an arrow. Now it says the wallet is
+// locked and opens the Accounts page, remembering the feed for the way back.
+// Run: bun tools/zap-locked-test.js
+import puppeteer from './puppeteer.js';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+import { accountXpubFor, cacheKeyFor } from '../src/wallet.js';
+import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
+import { buildHtml } from '../build.js';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+
+const xpub = accountXpubFor({ mnemonic: generateMnemonic(wordlist) });
+const SK = generateSecretKey(), AUTHOR = getPublicKey(SK);
+const now = Math.floor(Date.now() / 1000);
+const seeded = [finalizeEvent({ kind: 1, created_at: now - 100, tags: [], content: 'a sunset worth a zap' }, SK)];
+
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 0, fetch: (req) => {
+ const path = new URL(req.url).pathname;
+ if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path));
+ return new Response(html, { headers: { 'content-type': 'text/html' } });
+} });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const errs = [];
+page.on('pageerror', (e) => errs.push(String(e)));
+const waitFor = async (fn, arg, ms = 10000) => { for (let i = 0; i < ms / 200; i++) { if (await page.evaluate(fn, arg)) return true; await sleep(200); } return false; };
+try {
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
+ const base = cacheKeyFor(xpub);
+ await page.evaluate(([xpub, k, pk, ns]) => {
+ // a saved wallet, locked: only its xpub and its nostr identity on this device
+ localStorage.setItem('btc-wallet-watch', JSON.stringify([{ id: 'locked-watch', label: 'Account 1', xpub, network: 'mainnet', nostrPk: pk }]));
+ localStorage.setItem('btc-wallet-lockpw', '1');
+ localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) }));
+ localStorage.setItem(k + ':feedNotes', JSON.stringify(ns));
+ localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Sunset Poster', t: Date.now() } }));
+ localStorage.setItem('btc-wallet-feed:' + k, 'following');
+ }, [xpub, base, AUTHOR, seeded]);
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('.header-avatar', { timeout: 20000 });
+ await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click());
+ check('the post is in the feed', await waitFor(() => !!document.querySelector('.notes-feed .note-zap'), null, 20000));
+ const before = await page.evaluate(() => { const b = document.querySelector('.notes-feed .note-zap'); return { disabled: b.getAttribute('aria-disabled'), cursor: getComputedStyle(b).cursor }; });
+ check('the ⚡ button is live (not aria-disabled, a pointer cursor)', before.disabled !== 'true' && before.cursor === 'pointer', JSON.stringify(before));
+ await page.evaluate(() => document.querySelector('.notes-feed .note-zap').click());
+ check('the tap says the wallet is locked', await waitFor(() => /locked, so it can.t zap/i.test(document.body.innerText), null, 4000));
+ check('…and opens the Accounts page', await waitFor(() => !document.querySelector('.notes-feed') && /add account/i.test(document.body.innerText), null, 4000),
+ (await page.evaluate(() => document.body.innerText.slice(0, 160))).replace(/\n+/g, ' | '));
+ check('the feed is remembered for the way back', await page.evaluate(() => /^(\/feed|feed:)/.test(sessionStorage.getItem('coinos-after-signin') || '')));
+ check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
+} finally { await browser.close(); server.stop(true); }
+console.log(ok ? '\n✅ a locked wallet\'s ⚡ says so and opens the Accounts page' : '\n❌ failed');
+process.exit(ok ? 0 : 1);