aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/gifts.js8
-rw-r--r--src/features/messages.js89
-rw-r--r--src/features/withdraw-links.js195
-rw-r--r--src/i18n.js29
-rw-r--r--tools/account-switch-leak-test.js6
-rw-r--r--tools/amber-dm-upgrade-test.js6
-rw-r--r--tools/avatar-profile-test.js6
-rw-r--r--tools/call-test.js6
-rw-r--r--tools/call-wake-test.js6
-rw-r--r--tools/comments-test.js6
-rw-r--r--tools/composer-media-test.js6
-rw-r--r--tools/dm-nip42-test.js6
-rw-r--r--tools/dm-row-resort-test.js6
-rw-r--r--tools/feed-appetite-test.js6
-rw-r--r--tools/feed-conversations-test.js6
-rw-r--r--tools/feed-custom-test.js6
-rw-r--r--tools/feed-list-test.js6
-rw-r--r--tools/feed-media-cache-test.js6
-rw-r--r--tools/feed-media-views-test.js6
-rw-r--r--tools/feed-mention-tap-test.js6
-rw-r--r--tools/feed-pager-ahead-test.js6
-rw-r--r--tools/feed-picture-pager-test.js6
-rw-r--r--tools/feed-reboost-test.js6
-rw-r--r--tools/feed-repost-test.js6
-rw-r--r--tools/feed-switch-seed-test.js6
-rw-r--r--tools/feed-video-aspect-test.js6
-rw-r--r--tools/feed-video-pager-test.js6
-rw-r--r--tools/feed-window-dom-test.js6
-rw-r--r--tools/fullscreen-notice-test.js6
-rw-r--r--tools/gif-picker-test.js6
-rw-r--r--tools/go-live-test.js6
-rw-r--r--tools/grid-early-player-test.js6
-rw-r--r--tools/history-export-test.js6
-rw-r--r--tools/image-tap-test.js6
-rw-r--r--tools/lightbox-scroll-test.js6
-rw-r--r--tools/link-preview-test.js6
-rw-r--r--tools/live-stream-test.js6
-rw-r--r--tools/lnurl-withdraw-links-test.js166
-rw-r--r--tools/media-grid-dups-test.js6
-rw-r--r--tools/media-grid-order-test.js6
-rw-r--r--tools/media-moments-feed-test.js6
-rw-r--r--tools/mention-ac-test.js6
-rw-r--r--tools/moderation-test.js6
-rw-r--r--tools/negentropy-sync-test.js6
-rw-r--r--tools/notif-categories-test.js4
-rw-r--r--tools/notif-custom-emoji-test.js6
-rw-r--r--tools/notif-faces-test.js6
-rw-r--r--tools/notif-note-cache-test.js6
-rw-r--r--tools/notif-report-fold-test.js6
-rw-r--r--tools/nwc-settings-ui-test.js6
-rw-r--r--tools/onboard-cover-test.js4
-rw-r--r--tools/own-post-feeds-test.js6
-rw-r--r--tools/pager-caption-test.js6
-rw-r--r--tools/pager-close-test.js6
-rw-r--r--tools/pager-controls-test.js6
-rw-r--r--tools/pager-eager-test.js6
-rw-r--r--tools/pager-first-frame-test.js6
-rw-r--r--tools/pager-gone-picture-test.js6
-rw-r--r--tools/pager-no-poster-test.js6
-rw-r--r--tools/pager-prime-test.js6
-rw-r--r--tools/pager-ready-gate-test.js6
-rw-r--r--tools/pager-resize-test.js6
-rw-r--r--tools/pager-rotate-test.js6
-rw-r--r--tools/pager-seek-hide-test.js6
-rw-r--r--tools/pager-sharpen-test.js6
-rw-r--r--tools/pager-swipe-fly-test.js6
-rw-r--r--tools/pager-tail-test.js6
-rw-r--r--tools/pager-tap-slop-test.js6
-rw-r--r--tools/pager-wheel-test.js6
-rw-r--r--tools/pic-proxy-test.js6
-rw-r--r--tools/picture-pager-ready-test.js6
-rw-r--r--tools/poll-test.js6
-rw-r--r--tools/post-edit-test.js6
-rw-r--r--tools/profile-pager-test.js6
-rw-r--r--tools/profile-repost-test.js6
-rw-r--r--tools/puppeteer.js17
-rw-r--r--tools/quote-pager-test.js6
-rw-r--r--tools/quoted-clip-fullscreen-test.js6
-rw-r--r--tools/relay-budget-test.js6
-rw-r--r--tools/relays-settings-test.js6
-rw-r--r--tools/resume-wake-test.js6
-rw-r--r--tools/room-faces-test.js6
-rw-r--r--tools/scheduled-post-test.js6
-rw-r--r--tools/sensitive-feed-test.js6
-rw-r--r--tools/signer-resume-test.js6
-rw-r--r--tools/slow-image-test.js6
-rw-r--r--tools/standalone-file-test.js4
-rw-r--r--tools/thread-instant-test.js6
-rw-r--r--tools/translate-phone-lang-test.js6
-rw-r--r--tools/translate-test.js6
-rw-r--r--tools/video-autoplay-test.js6
-rw-r--r--tools/video-compress-test.js6
-rw-r--r--tools/video-events-test.js6
-rw-r--r--tools/video-fallback-test.js6
-rw-r--r--tools/video-gone-test.js6
-rw-r--r--tools/video-hls-play-test.js6
-rw-r--r--tools/video-tap-test.js6
-rw-r--r--tools/video-tile-poster-test.js6
-rw-r--r--tools/video-touch-tap-test.js6
-rw-r--r--tools/web-comment-test.js6
-rw-r--r--tools/wn-only-device-test.js6
-rw-r--r--tools/youtube-tap-test.js6
102 files changed, 769 insertions, 305 deletions
diff --git a/src/features/gifts.js b/src/features/gifts.js
index f23ecc8..c6ccbd9 100644
--- a/src/features/gifts.js
+++ b/src/features/gifts.js
@@ -8,6 +8,7 @@ import { decryptWithCode, npubOf } from '../nostr.js';
import { installGiftWallet, previewGift, giftOutpoints, giftClaimTxs, isRetiredGift, giftMinimum, lockGift, previewLockedGift } from './gifts-wallet.js';
import { t } from '../i18n.js';
import { qrSvg } from '../qr.js';
+import { withdrawLinks } from './withdraw-links.js';
import { fmtBtc, timeAgo, shortAddr } from '../format.js';
import { getEventHash, verifyEvent } from 'nostr-tools/pure';
import { sha256 } from '@noble/hashes/sha256';
@@ -26,6 +27,7 @@ export function giftsFeature(ctx) {
// every call degrades to null in an on-chain-only build.
const hook = ctx.hook || (() => null);
const arkGiftOf = (code) => hook('arkGiftDecode', code);
+ const wl = withdrawLinks(ctx); // prepaid LNURL-withdraw links, the gift link's sibling on Send
// ================================================================ GIFT / CLAIM
// Read a gift link, returning the code (and scrubbing it from the URL so the
@@ -1085,9 +1087,11 @@ export function giftsFeature(ctx) {
return { blob, claimCode, url: `${location.origin}/lg/${blob}` };
},
receiveTakeover() { return claimCelebration(); },
- sendView() { return ui.giftMode ? giftView() : null; },
+ sendView() { return ui.giftMode === 'lnurl' ? wl.view() : ui.giftMode ? giftView() : null; },
sendFormExtras() {
- return h('button', { class: 'linklike small', style: 'align-self:center;margin-top:2px', onClick: () => { ui.giftMode = true; ui.sendError = ''; render(); } }, '๐ŸŽ ' + t('giftLink'));
+ return h('div', { class: 'row gap6 wrap', style: 'justify-content:center;column-gap:18px' },
+ h('button', { class: 'linklike small', style: 'margin-top:2px', onClick: () => { ui.giftMode = true; ui.sendError = ''; render(); } }, '๐ŸŽ ' + t('giftLink')),
+ wl.entry());
},
balanceLines() {
const locked = wallet.giftLockedValue();
diff --git a/src/features/messages.js b/src/features/messages.js
index 90bfaaf..6c4f5f8 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -760,14 +760,38 @@ export function messagesFeature(ctx) {
let allUnsubs = [];
let dmUnsubs = []; // the DM wrap subscriptions alone, so they can be rebuilt
+ // Is the page under the reader's finger, or still coasting from a flick?
+ // Background work (a repaint for a face that arrived, writing the profile
+ // cache) waits for it to settle: on Adam's Pixel a flick through the feed
+ // spent 1.7 s of 9 on the main thread doing exactly that, and taps queued
+ // behind it (2026-11-07, "flick to scroll โ€ฆ not getting up to speed โ€ฆ
+ // clicking around feels sluggish"). Never longer than `most` ms, though:
+ // a slow, steady read must still see what arrived.
+ let pageMovedAt = 0, fingerDown = false;
+ if (typeof window !== 'undefined') {
+ window.addEventListener('scroll', () => { pageMovedAt = Date.now(); }, { passive: true, capture: true });
+ window.addEventListener('touchstart', () => { fingerDown = true; }, { passive: true, capture: true });
+ for (const n of ['touchend', 'touchcancel']) window.addEventListener(n, (e) => { if (!e.touches || !e.touches.length) fingerDown = false; }, { passive: true, capture: true });
+ }
+ const pageMoving = () => fingerDown || Date.now() - pageMovedAt < 200;
+ const whenSettled = (fn, most = 1500, step = 150) => {
+ const until = Date.now() + most;
+ const go = () => { if (pageMoving() && Date.now() < until) { setTimeout(go, step); return; } fn(); };
+ go();
+ };
+
let repaintTimer = null;
const scheduleRepaint = () => {
if (repaintTimer) return;
+ const since = Date.now();
const run = () => {
// not under a pager in motion: the page beneath is out of sight, and
// its repaint (40โ€“55 ms on Adam's phone) dropped frames from flights
// through the pictures (2026-10-17) โ€” it waits for the landing
if (pagerMoving()) { repaintTimer = setTimeout(run, 120); return; }
+ // nor mid-flick on the page itself (up to 1.5 s): a whole-page morph
+ // in the middle of a fling is a dropped frame and a late tap
+ if (pageMoving() && Date.now() - since < 1500) { repaintTimer = setTimeout(run, 150); return; }
repaintTimer = null;
// a visitor's public page (a profile, a thread, a feed) repaints too
if (ui.screen === 'wallet' || ui.pubProf) renderThreadStable();
@@ -826,11 +850,14 @@ export function messagesFeature(ctx) {
// memory and is written back once per burst: a batch of thirty profiles
// landing during a scroll used to parse and re-serialize the whole blob
// thirty times over, on the main thread, mid-scroll.
- let profBlob = null, profFlush = null;
+ let profBlob = null, profFlush = null, profFlushSince = 0;
const PROFILE_ROWS = 1500; // a row without a thumbnail is ~430 chars; IndexedDB has room
const profilesDisk = () => (profBlob ||= wallet.loadFeatureState('profiles', {}));
- function flushProfiles() {
- profFlush = null;
+ function flushProfiles(force) {
+ // up to 5 s for the page to settle: stringifying this blob (thumbnails
+ // and all, up to ~2 MB) is a long main-thread task
+ if (!force && pageMoving() && Date.now() - (profFlushSince || 0) < 5000) { profFlush = setTimeout(flushProfiles, 300); return; }
+ profFlush = null; profFlushSince = 0;
const s = profBlob;
if (!s) return;
// Who stays when space runs out: your own faces, the people you follow,
@@ -848,7 +875,10 @@ export function messagesFeature(ctx) {
// Thumbnails are the bulk of this blob, so they live on a budget: the
// least recently seen strangers give theirs up first. The row itself
// stays โ€” that face just paints the way it used to.
- let size = JSON.stringify(s).length;
+ // (an estimate, not a JSON.stringify of the lot just to measure it:
+ // a row is ~430 chars plus its thumbnail)
+ let size = 0;
+ for (const k in s) size += 430 + ((s[k] && s[k].thumb) || '').length;
if (size > THUMB_BUDGET) {
// never our own: the header face is the one that must paint offline
for (const k of evictFirst(Object.keys(s).filter((k) => s[k].thumb && !mine.has(k)))) {
@@ -875,7 +905,7 @@ export function messagesFeature(ctx) {
? { thumb: p.thumb, thumbFor: p.thumbFor, thumbPx: p.thumbPx || 0 } : {}),
...(p.thumbFail ? { thumbFail: p.thumbFail, thumbFailAt: p.thumbFailAt || 0, thumbFails: p.thumbFails || 1,
thumbFailVersion: p.thumbFailVersion || 0 } : {}) };
- if (!profFlush) profFlush = setTimeout(flushProfiles, 500);
+ if (!profFlush) { profFlushSince = Date.now(); profFlush = setTimeout(flushProfiles, 500); }
}
// A punk picture is OUR OWN art. Every coinos user who keeps the default
// publishes https://v3.coinos.io/punks/N.webp as their nostr picture, and
@@ -1081,6 +1111,39 @@ export function messagesFeature(ctx) {
blobProbing.set(url, task);
return task;
}
+ // Centre-crop to a square (the way background-size:cover paints it),
+ // shrink, and encode โ€” off the main thread where the browser allows: the
+ // crop and resize through createImageBitmap, the encode through an
+ // OffscreenCanvas's convertToBlob. The synchronous canvas.toDataURL it
+ // replaces cost ~30 ms a face on a phone, mid-scroll (2026-11-07).
+ async function encodeThumb(bmp) {
+ const side = Math.min(bmp.width, bmp.height);
+ if (!side) throw new Error('empty');
+ const sx = (bmp.width - side) / 2, sy = (bmp.height - side) / 2;
+ if (typeof OffscreenCanvas !== 'undefined' && OffscreenCanvas.prototype.convertToBlob) {
+ let small = null;
+ try {
+ small = await createImageBitmap(bmp, sx, sy, side, side, { resizeWidth: THUMB_PX, resizeHeight: THUMB_PX, resizeQuality: 'high' });
+ const oc = new OffscreenCanvas(THUMB_PX, THUMB_PX);
+ oc.getContext('2d').drawImage(small, 0, 0);
+ const asUrl = (b) => new Promise((ok, bad) => { const r = new FileReader(); r.onload = () => ok(r.result); r.onerror = () => bad(r.error); r.readAsDataURL(b); });
+ let blob = await oc.convertToBlob({ type: 'image/webp', quality: 0.75 });
+ if (blob.type !== 'image/webp') blob = await oc.convertToBlob({ type: 'image/jpeg', quality: 0.7 });
+ if (blob.size * 4 / 3 > THUMB_MAX) blob = await oc.convertToBlob({ type: 'image/webp', quality: 0.5 }); // a busy picture, leaned on harder
+ const data = await asUrl(blob);
+ if (data.length > THUMB_MAX) throw new Error('too big');
+ return data;
+ } finally { try { small && small.close(); } catch {} }
+ }
+ const c = document.createElement('canvas');
+ c.width = c.height = THUMB_PX;
+ c.getContext('2d').drawImage(bmp, sx, sy, side, side, 0, 0, THUMB_PX, THUMB_PX);
+ let data = c.toDataURL('image/webp', 0.75);
+ if (!data.startsWith('data:image/webp')) data = c.toDataURL('image/jpeg', 0.7);
+ if (data.length > THUMB_MAX) data = c.toDataURL('image/webp', 0.5);
+ if (data.length > THUMB_MAX) throw new Error('too big');
+ return data;
+ }
function makeThumb(pk, p) {
if (!p || !p.picture || typeof document === 'undefined') return;
if (noCorsHost(p.picture)) return; // a host that won't hand over bytes; the <img> shows it anyway
@@ -1152,18 +1215,8 @@ export function messagesFeature(ctx) {
scheduleRepaint(); // the big face, where one is up, can paint from the copy now
}
bmp = await createImageBitmap(await res.blob());
- // centre-crop to a square, the way background-size:cover paints it
- const side = Math.min(bmp.width, bmp.height);
- if (!side) throw new Error('empty');
- const c = document.createElement('canvas');
- c.width = c.height = THUMB_PX;
- c.getContext('2d').drawImage(bmp, (bmp.width - side) / 2, (bmp.height - side) / 2,
- side, side, 0, 0, THUMB_PX, THUMB_PX);
- let data = c.toDataURL('image/webp', 0.75);
- if (!data.startsWith('data:image/webp')) data = c.toDataURL('image/jpeg', 0.7);
- if (data.length > THUMB_MAX) data = c.toDataURL('image/webp', 0.5); // a busy picture, leaned on harder
- if (data.length > THUMB_MAX) throw new Error('too big');
- done({ thumb: data, thumbFor: url, thumbPx: THUMB_PX });
+ const data = await encodeThumb(bmp);
+ whenSettled(() => done({ thumb: data, thumbFor: url, thumbPx: THUMB_PX }));
} catch (e) {
// a quick TypeError is the CORS refusal (a dead host takes longer)
// only a refusal corsFetch confirmed (a body cut off mid-download
@@ -19062,7 +19115,7 @@ export function messagesFeature(ctx) {
follows = null; followsAt = 0; feed = null; relayLists = null;
mutes = null; mutesAt = 0;
stCache = null; stAt = 0; // the next account's state, not this one's
- if (profFlush) { clearTimeout(profFlush); flushProfiles(); } // this account's faces, written before the blob is dropped
+ if (profFlush) { clearTimeout(profFlush); flushProfiles(true); } // this account's faces, written before the blob is dropped
profBlob = null;
timedActivity.clear(); diskHeld.clear(); reacts.clear(); boosts.clear(); seenNoteEv.clear(); myReactEv.clear(); quoted.clear();
noteCountsReady.clear();
diff --git a/src/features/withdraw-links.js b/src/features/withdraw-links.js
new file mode 100644
index 0000000..a63d180
--- /dev/null
+++ b/src/features/withdraw-links.js
@@ -0,0 +1,195 @@
+// Prepaid Lightning withdraw links (LNURL-withdraw): make a link or QR that
+// whoever holds it can pull sats from โ€” a voucher, a faucet, a printed card โ€”
+// even while this wallet is asleep. "Do we have lnurl withdraw links
+// somewhere? can we add them?" โ†’ "Prepaid" (Adam, 2026-11-07).
+//
+// The money is paid up front, from Spending over Lightning, to the coinos
+// registrar (coinosv3-server names/withdraw.js), which pays each claim out of
+// it; until claimed it sits there, and the creator can take back whatever is
+// left at any time ("Take back"), which closes the link. Links belong to the
+// wallet's own nostr key, so every device with the seed sees them.
+//
+// It lives on the Send page as a sibling of the gift link: ui.giftMode ===
+// 'lnurl' (giftMode is already a navigation field with its resets).
+import { nip98Header } from '../nip98.js';
+import { encodeLnurl } from '../lnurl.js';
+import { qrSvg } from '../qr.js';
+import { getNetwork } from '../api.js';
+import { t } from '../i18n.js';
+
+const REGISTRAR = 'https://names.coinos.io';
+const NOTE_MAX = 120;
+const net = () => (getNetwork() === 'mutinynet' ? 'mutinynet' : 'mainnet');
+const available = () => ['mainnet', 'mutinynet'].includes(getNetwork());
+
+export function withdrawLinks(ctx) {
+ const { h, ui, render, wallet, toast, copyBtn, goBack, fmtAmount, unitLabel, unitTag, parseAmount, getUnit } = ctx;
+ const hook = ctx.hook || (() => null);
+ const base = () => ctx.withdrawRegistrar || REGISTRAR;
+ // ui.wl: { list: [...]|null, listError, amount, uses, note, busy, error, open: id|null, made: id|null, taking: id|null }
+ const st = () => (ui.wl ||= { list: null, amount: '', uses: '1', note: '' });
+
+ const signer = { signEvent: (e) => wallet.nostrSign(e) };
+ async function call(method, path, payload) {
+ const url = base() + path;
+ const body = payload ? JSON.stringify(payload) : '';
+ const auth = await nip98Header(signer, url, method, body);
+ const r = await fetch(url, { method, headers: { authorization: auth, ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body } : {}) });
+ const j = await r.json().catch(() => ({}));
+ if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`);
+ return j;
+ }
+
+ async function load() {
+ const s = st();
+ try {
+ const j = await call('GET', '/withdraw/mine');
+ s.list = (j.links || []).filter((l) => l.net === net());
+ s.listError = '';
+ } catch (e) { s.listError = e.message; s.list ||= []; }
+ render();
+ }
+
+ function open() {
+ ui.wl = null;
+ ui.giftMode = 'lnurl';
+ ui.sendError = '';
+ st();
+ render();
+ load();
+ }
+
+ const lnurlOf = (l) => encodeLnurl(l.url).toUpperCase();
+ const amt = (sat) => fmtAmount(sat) + ' ' + unitLabel();
+
+ async function create() {
+ const s = st();
+ const sat = parseAmount(s.amount, getUnit()) || 0;
+ const uses = Math.floor(Number(s.uses) || 1);
+ s.error = '';
+ if (!(sat >= 1)) { s.error = t('wlAmountInvalid'); render(); return; }
+ if (!(uses >= 1 && uses <= 1000)) { s.error = t('wlUsesInvalid'); render(); return; }
+ if (wallet.watchOnly) { s.error = t('lnWithdrawWatchOnly'); render(); return; }
+ if (!hook('arkReady')) { s.error = t('hatNeedSpending'); render(); return; }
+ const total = sat * uses;
+ if ((hook('arkSpendableSat') || 0) < total + Math.max(10, Math.ceil(total * 0.002))) { s.error = t('wlNoFunds', { amount: amt(total) }); render(); return; }
+ s.busy = true; render();
+ try {
+ const inv = await call('POST', '/withdraw/new', { sat, uses, note: String(s.note || '').trim().slice(0, NOTE_MAX), net: net() });
+ await hook('arkPayInvoice', inv.invoice, { maxAmountSat: inv.totalSat });
+ let link = null;
+ for (let i = 0; i < 10 && !link; i++) {
+ try { link = await call('POST', '/withdraw/funded', { id: inv.id }); }
+ catch (e) { if (!/not paid/i.test(e.message)) throw e; await new Promise((r) => setTimeout(r, 1500)); }
+ }
+ if (!link) throw new Error(t('wlNotConfirmed'));
+ s.list = [link, ...(s.list || []).filter((l) => l.id !== link.id)];
+ s.made = link.id; s.open = link.id;
+ s.amount = ''; s.uses = '1'; s.note = '';
+ } catch (e) {
+ s.error = e.message || String(e);
+ } finally { s.busy = false; render(); }
+ }
+
+ async function takeBack(l) {
+ const s = st();
+ s.taking = l.id; s.error = ''; render();
+ let lnId = null;
+ try {
+ if (l.remainingSat > 0) {
+ const inv = await hook('arkLnInvoice', l.remainingSat, t('wlTakeBackMemo'));
+ if (!inv || !inv.invoice) throw new Error(t('lnWithdrawNoInvoice'));
+ lnId = inv.id;
+ // the wallet has to claim the held payment while the registrar pays it
+ hook('arkLnWatch', inv.id, () => {});
+ const r = await call('POST', '/withdraw/reclaim', { id: l.id, invoice: inv.invoice });
+ Object.assign(l, r);
+ toast(r.paid === false ? t('wlTakeBackFailed') : t('wlTakenBack', { amount: amt(inv.amountSat) }));
+ } else {
+ Object.assign(l, await call('POST', '/withdraw/reclaim', { id: l.id }));
+ }
+ } catch (e) {
+ if (lnId) hook('arkLnCancel', lnId);
+ s.error = e.message || String(e);
+ } finally { s.taking = null; render(); load(); }
+ }
+
+ function linkCard(l) {
+ const s = st();
+ const lnurl = lnurlOf(l);
+ let svg = null;
+ try { svg = qrSvg(lnurl, { ec: 'M', mode: 'Alphanumeric' }); } catch {}
+ const live = l.status === 'open' && l.remainingSat > 0 && l.used < l.uses;
+ return h('div', { class: 'card col', style: 'align-items:center;gap:10px' },
+ h('h3', { style: 'align-self:stretch' }, s.made === l.id ? t('wlReady') : t('wlTitle')),
+ live && svg ? h('div', { html: svg }) : null,
+ l.note ? h('blockquote', { class: 'gift-note', style: 'width:100%' }, l.note) : null,
+ h('div', { class: 'small muted', style: 'text-align:center' },
+ l.uses > 1 ? t('wlTermsMany', { amount: amt(l.sat), n: l.uses - l.used, total: l.uses }) : t('wlTermsOne', { amount: amt(l.sat) }),
+ ' ยท ', t('wlLeft', { amount: amt(l.remainingSat) })),
+ live ? h('div', { class: 'addr-box break mono', style: 'width:100%;font-size:11px' }, lnurl) : h('div', { class: 'notice small' }, l.status === 'closed' ? t('wlClosed') : t('wlUsedUp')),
+ live ? h('div', { class: 'row gap6 wrap', style: 'width:100%' }, copyBtn(lnurl, t('wlCopy'))) : null,
+ live ? h('div', { class: 'small faint', style: 'text-align:center' }, t('wlBearer')) : null,
+ s.error && h('div', { class: 'notice err', style: 'width:100%' }, s.error),
+ live || (l.status === 'open' && l.remainingSat > 0)
+ ? (s.taking === l.id
+ ? h('button', { class: 'btn-block', disabled: true }, h('span', { class: 'spinner sm' }))
+ : h('button', { class: 'btn-ghost btn-block', onClick: () => takeBack(l) }, t('wlTakeBack', { amount: amt(l.remainingSat) })))
+ : null,
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { s.open = null; s.made = null; s.error = ''; render(); } }, t('back')));
+ }
+
+ function formCard() {
+ const s = st();
+ const sat = parseAmount(s.amount, getUnit()) || 0;
+ const uses = Math.max(1, Math.floor(Number(s.uses) || 1));
+ return h('div', { class: 'card col gap6' },
+ h('h3', {}, t('wlTitle')),
+ h('div', { class: 'small muted' }, t('wlDesc')),
+ h('div', { class: 'input-group' },
+ h('input', { id: 'wl-amount', type: 'number', min: '0', inputmode: 'decimal', step: getUnit() === 'sats' ? '1' : '0.00000001',
+ placeholder: t('wlAmount'), value: s.amount, onInput: (e) => { s.amount = e.target.value; render(); } }),
+ h('div', { style: 'display:flex;align-items:center' }, unitTag())),
+ h('label', { class: 'row gap6', style: 'align-items:center' },
+ h('span', { class: 'small grow' }, t('wlUses')),
+ h('input', { id: 'wl-uses', type: 'number', min: '1', max: '1000', step: '1', inputmode: 'numeric', style: 'width:90px',
+ value: s.uses, onInput: (e) => { s.uses = e.target.value; render(); } })),
+ h('textarea', { class: 'gift-note-input', rows: 2, maxlength: NOTE_MAX, placeholder: t('wlNote'), value: s.note, onInput: (e) => { s.note = e.target.value; } }),
+ h('div', { class: 'small faint' }, sat >= 1 && uses > 1 ? t('wlPayNowMany', { total: amt(sat * uses), n: uses, amount: amt(sat) }) : t('wlPayNow')),
+ s.error && !s.open ? h('div', { class: 'notice err' }, s.error) : null,
+ s.busy
+ ? h('button', { class: 'btn-primary btn-block', disabled: true }, h('span', { class: 'spinner sm' }))
+ : h('button', { id: 'wl-create', class: 'btn-primary btn-block', onClick: create }, sat >= 1 ? t('wlCreateFor', { amount: amt(sat * uses) }) : t('wlCreate')));
+ }
+
+ function listCard() {
+ const s = st();
+ if (s.list == null) return h('div', { class: 'card', style: 'text-align:center' }, h('span', { class: 'spinner sm' }));
+ if (!s.list.length) return s.listError ? h('div', { class: 'notice err small' }, s.listError) : null;
+ return h('div', { class: 'card col gap6' },
+ h('span', { class: 'small muted' }, t('wlYours')),
+ ...s.list.map((l) => h('button', { class: 'row between wl-row', style: 'text-align:left', onClick: () => { s.open = l.id; s.made = null; s.error = ''; render(); } },
+ h('span', { class: 'small' }, amt(l.sat), l.uses > 1 ? ` ร— ${l.uses}` : '', l.note ? h('span', { class: 'faint' }, ' ยท ' + l.note) : null),
+ h('span', { class: 'small faint' },
+ l.status === 'closed' ? t('wlClosedTag') : l.remainingSat < 1 || l.used >= l.uses ? t('wlUsedUpTag') : t('wlLeft', { amount: amt(l.remainingSat) })))));
+ }
+
+ function view() {
+ const s = st();
+ if (!available()) {
+ return h('div', { class: 'col', style: 'gap:12px' },
+ h('div', { class: 'card col' }, h('h3', {}, t('wlTitle')), h('div', { class: 'small muted' }, t('wlMainnetOnly'))),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => goBack(() => { ui.giftMode = false; }) }, t('back')));
+ }
+ const l = s.open && (s.list || []).find((x) => x.id === s.open);
+ if (l) return h('div', { class: 'col', style: 'gap:12px' }, linkCard(l));
+ return h('div', { class: 'col', style: 'gap:12px' },
+ formCard(),
+ listCard(),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.wl = null; goBack(() => { ui.giftMode = false; }); } }, t('back')));
+ }
+
+ const entry = () => h('button', { id: 'wl-open', class: 'linklike small', style: 'align-self:center;margin-top:2px', onClick: open }, 'โšก ' + t('wlTitle'));
+
+ return { view, entry, open };
+}
diff --git a/src/i18n.js b/src/i18n.js
index b45638e..96f4548 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1743,6 +1743,35 @@ const STR = {
lnWithdrawPaid: 'Received {amount}',
lnWithdrawNotPaid: 'They took the invoice but the payment didnโ€™t arrive. Check with them before trying again.',
lnWithdrawNoInvoice: 'Couldnโ€™t make an invoice to be paid to.',
+ wlTitle: 'Withdraw link',
+ wlReady: 'Your withdraw link is ready',
+ wlDesc: 'A Lightning link or QR code that anyone can withdraw sats from, like a voucher. It works with most Lightning wallets, even while your phone is off. You pay for it up front; Coinos holds the sats until theyโ€™re claimed, and you can take back whatever is left at any time.',
+ wlAmount: 'Amount per withdrawal',
+ wlUses: 'How many withdrawals',
+ wlNote: 'Description (optional)',
+ wlPayNow: 'Paid now from Spending.',
+ wlPayNowMany: '{total} paid now from Spending: {n} withdrawals of up to {amount}.',
+ wlCreate: 'Create withdraw link',
+ wlCreateFor: 'Create link for {amount}',
+ wlAmountInvalid: 'Enter an amount.',
+ wlUsesInvalid: 'Between 1 and 1000 withdrawals.',
+ wlNoFunds: 'Not enough in Spending for {amount}.',
+ wlNotConfirmed: 'Paid, but the link isnโ€™t confirmed yet. It will appear under your links in a moment.',
+ wlTermsOne: 'Up to {amount}, once',
+ wlTermsMany: 'Up to {amount} each, {n} of {total} withdrawals left',
+ wlLeft: '{amount} left',
+ wlCopy: 'Copy LNURL',
+ wlBearer: 'Anyone with this link or QR code can withdraw from it. Share it only with whoever itโ€™s for.',
+ wlTakeBack: 'Take back {amount}',
+ wlTakeBackMemo: 'Withdraw link taken back',
+ wlTakenBack: '{amount} is back in Spending',
+ wlTakeBackFailed: 'Couldnโ€™t pay it back just now. The link stays open; try again in a moment.',
+ wlClosed: 'This link is closed.',
+ wlUsedUp: 'This link has been used up.',
+ wlClosedTag: 'closed',
+ wlUsedUpTag: 'used up',
+ wlYours: 'Your withdraw links',
+ wlMainnetOnly: 'Withdraw links are only available on mainnet.',
lnWithdrawWatchOnly: 'This device canโ€™t receive: the wallet is watch-only here.',
lnWithdrawMemo: 'Withdrawal from {domain}',
lnPayAmount: 'Amount',
diff --git a/tools/account-switch-leak-test.js b/tools/account-switch-leak-test.js
index ea3b4ee..aa06579 100644
--- a/tools/account-switch-leak-test.js
+++ b/tools/account-switch-leak-test.js
@@ -3,7 +3,7 @@
// under its key, so a throwaway account showed adam's zaps and reactions for
// good. Also: a DM cache already polluted that way is swept on load.
// Run: bun tools/account-switch-leak-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -61,9 +61,9 @@ const nav = (label) => page.evaluate((l) => document.querySelector(`.app-nav-but
async function newWallet() {
await click('create a new wallet'); await sleep(600);
if (!(await page.$('.words .w .t'))) { await click('generate'); await sleep(600); }
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
return mn;
}
diff --git a/tools/amber-dm-upgrade-test.js b/tools/amber-dm-upgrade-test.js
index 250e8ef..3a1917a 100644
--- a/tools/amber-dm-upgrade-test.js
+++ b/tools/amber-dm-upgrade-test.js
@@ -7,7 +7,7 @@
// relays hold is opened again โ€” including ones the old code wrote off.
// The remote signer is played by this script over a shimmed relay.
// Run: bun tools/amber-dm-upgrade-test.js (and ALL_LOGGED=1 bun tools/amber-dm-upgrade-test.js)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import * as nip44 from 'nostr-tools/nip44';
@@ -102,9 +102,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
// an Amber login, and a wrap log in which the old code had already
diff --git a/tools/avatar-profile-test.js b/tools/avatar-profile-test.js
index 5c5293d..6d6c944 100644
--- a/tools/avatar-profile-test.js
+++ b/tools/avatar-profile-test.js
@@ -3,7 +3,7 @@
// wherever the reader was in the feed. Back returns to the same place.
//
// Run: bun tools/avatar-profile-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -25,9 +25,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const A = 'a'.repeat(63) + '9', B = 'b'.repeat(63) + '7';
diff --git a/tools/call-test.js b/tools/call-test.js
index d5c3909..44f9b46 100644
--- a/tools/call-test.js
+++ b/tools/call-test.js
@@ -5,7 +5,7 @@
// both. A call can be declined, and a stranger's (neither followed nor
// written to) doesn't ring, but is in the conversation as a missed call. Everything on the wire is an ephemeral gift wrap.
// Run: bun tools/call-test.js (NO_BUILD=1 to reuse dist/)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { getPublicKey } from 'nostr-tools/pure';
@@ -74,9 +74,9 @@ async function wallet(name) {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
return { page, mn, pk: getPublicKey(privateKeyFromSeedWords(mn)), base: cacheKeyFor(mn + '\n'), click, waitText, name };
}
diff --git a/tools/call-wake-test.js b/tools/call-wake-test.js
index b0f64cc..3f81db1 100644
--- a/tools/call-wake-test.js
+++ b/tools/call-wake-test.js
@@ -6,7 +6,7 @@
// they talk. A push for a stranger's call, or for something that isn't an
// offer, rings nothing. Plus the worker's check on its own (classifyCall).
// Run: bun tools/call-wake-test.js (NO_BUILD=1 to reuse dist/)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { getPublicKey, finalizeEvent, generateSecretKey } from 'nostr-tools/pure';
@@ -91,9 +91,9 @@ async function wallet(name) {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
const sk = privateKeyFromSeedWords(mn);
return { context, page, mn, sk, pk: getPublicKey(sk), base: cacheKeyFor(mn + '\n'), click, waitText, name };
diff --git a/tools/comments-test.js b/tools/comments-test.js
index 5a73e6f..21d028d 100644
--- a/tools/comments-test.js
+++ b/tools/comments-test.js
@@ -5,7 +5,7 @@
// are caught at the socket; nothing is published.
//
// Run: bun tools/comments-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { SimplePool } from 'nostr-tools/pool';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -86,8 +86,8 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.goto('http://localhost:5283/' + NEVENT, { waitUntil: 'domcontentloaded' });
const first = comments.sort((a, b) => a.created_at - b.created_at)[0];
diff --git a/tools/composer-media-test.js b/tools/composer-media-test.js
index 94b343a..db1648a 100644
--- a/tools/composer-media-test.js
+++ b/tools/composer-media-test.js
@@ -5,7 +5,7 @@
// paperclip, its URL landing in the draft.
//
// Run: bun tools/composer-media-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -42,9 +42,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.evaluate(() => document.querySelector('button[aria-label="Settings"]').click());
await sleep(300);
diff --git a/tools/dm-nip42-test.js b/tools/dm-nip42-test.js
index 51fa3a9..debe910 100644
--- a/tools/dm-nip42-test.js
+++ b/tools/dm-nip42-test.js
@@ -6,7 +6,7 @@
// copy of what we send in our own inbox so other clients see it too.
// Both inboxes are played by this script's WebSocket shim.
// Run: bun tools/dm-nip42-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent, verifyEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -87,9 +87,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const ME_SK = privateKeyFromSeedWords(mn); const ME = getPublicKey(ME_SK);
const base = cacheKeyFor(mn + '\n');
diff --git a/tools/dm-row-resort-test.js b/tools/dm-row-resort-test.js
index 9e400bc..0ecdd44 100644
--- a/tools/dm-row-resort-test.js
+++ b/tools/dm-row-resort-test.js
@@ -3,7 +3,7 @@
// it was first drawn for โ€” a tap on Bill's face opened someone else's
// profile. After the re-sort, each row's avatar and row open their own.
// Run: bun tools/dm-row-resort-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -67,9 +67,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('Receive', 20000);
const ME = getPublicKey(privateKeyFromSeedWords(mn));
const base = cacheKeyFor(mn + '\n');
diff --git a/tools/feed-appetite-test.js b/tools/feed-appetite-test.js
index b79a2a9..9f469cb 100644
--- a/tools/feed-appetite-test.js
+++ b/tools/feed-appetite-test.js
@@ -6,7 +6,7 @@
// still a while brings the ordinary page back. A page held still under an
// insert above is not travel.
// Run: bun tools/feed-appetite-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -79,9 +79,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-conversations-test.js b/tools/feed-conversations-test.js
index f16fc35..7394986 100644
--- a/tools/feed-conversations-test.js
+++ b/tools/feed-conversations-test.js
@@ -4,7 +4,7 @@
// profile's Replies tab shows the same context; the view persists across
// feeds and reloads like the other views.
// Run: bun tools/feed-conversations-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -48,9 +48,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, alice, bob, roots, replies]) => {
diff --git a/tools/feed-custom-test.js b/tools/feed-custom-test.js
index 122cb0a..c5ed41f 100644
--- a/tools/feed-custom-test.js
+++ b/tools/feed-custom-test.js
@@ -4,7 +4,7 @@
// feed, and deleting a feed. No relays are contacted for real โ€” the
// WebSocket shim answers the subscriptions.
// Run: bun tools/feed-custom-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -90,9 +90,9 @@ try {
await sleep(400);
// through the real first run: the wizard's own words, read off the screen
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
// the starter follows land once the current list has been asked for
diff --git a/tools/feed-list-test.js b/tools/feed-list-test.js
index 6d60240..00942b8 100644
--- a/tools/feed-list-test.js
+++ b/tools/feed-list-test.js
@@ -5,7 +5,7 @@
// unticking "Keep as a nostr list" sends the NIP-09 deletion. No relay is
// written for real โ€” the WebSocket shim swallows EVENT and answers OK.
// Run: bun tools/feed-list-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -83,9 +83,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME_SK = privateKeyFromSeedWords(mn);
diff --git a/tools/feed-media-cache-test.js b/tools/feed-media-cache-test.js
index 5a26ca2..8ca8185 100644
--- a/tools/feed-media-cache-test.js
+++ b/tools/feed-media-cache-test.js
@@ -3,7 +3,7 @@
// slow picture), a reload paints the grid from that cache at once and
// resumes the crawl where it stopped, and tile frames are kept as pictures.
// Run: bun tools/feed-media-cache-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -115,9 +115,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const key = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk]) => {
diff --git a/tools/feed-media-views-test.js b/tools/feed-media-views-test.js
index dac9804..74de24a 100644
--- a/tools/feed-media-views-test.js
+++ b/tools/feed-media-views-test.js
@@ -5,7 +5,7 @@
// until tapped: then the author, the words as a caption, likes and sats.
// Back and Escape close it.
// Run: bun tools/feed-media-views-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -112,9 +112,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const key = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-mention-tap-test.js b/tools/feed-mention-tap-test.js
index e4d3f7a..d381288 100644
--- a/tools/feed-mention-tap-test.js
+++ b/tools/feed-mention-tap-test.js
@@ -2,7 +2,7 @@
// thread on top of them (the mention's tap reached the row, which opened
// the thread over the profile it had just shown). Links in a post likewise.
// Run: bun tools/feed-mention-tap-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
@@ -33,9 +33,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('Receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, a, f, ns]) => {
diff --git a/tools/feed-pager-ahead-test.js b/tools/feed-pager-ahead-test.js
index 057ac3e..0906034 100644
--- a/tools/feed-pager-ahead-test.js
+++ b/tools/feed-pager-ahead-test.js
@@ -3,7 +3,7 @@
// feed pages on, and the pager grows โ€” past a run of posts with nothing to
// show, too โ€” so the reader never meets the end while there is more.
// Run: bun tools/feed-pager-ahead-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -96,9 +96,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-picture-pager-test.js b/tools/feed-picture-pager-test.js
index 4cbe68c..f74c981 100644
--- a/tools/feed-picture-pager-test.js
+++ b/tools/feed-picture-pager-test.js
@@ -4,7 +4,7 @@
// closes it, as before; once the reader has moved on, a tap shows the post's
// details and the next tap (or Escape) closes the pager onto that post.
// Run: bun tools/feed-picture-pager-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -67,9 +67,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-reboost-test.js b/tools/feed-reboost-test.js
index e22df91..f43e7b5 100644
--- a/tools/feed-reboost-test.js
+++ b/tools/feed-reboost-test.js
@@ -4,7 +4,7 @@
// reload โ€” stays out. A repost of a post you haven't read still lands, and
// the row you read keeps its place.
// Run: bun tools/feed-reboost-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -95,9 +95,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pks]) => {
diff --git a/tools/feed-repost-test.js b/tools/feed-repost-test.js
index 4936dd6..1bf3d7a 100644
--- a/tools/feed-repost-test.js
+++ b/tools/feed-repost-test.js
@@ -4,7 +4,7 @@
// copy doesn't check out is left out, and a note already in the feed isn't
// shown twice.
// Run: bun tools/feed-repost-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -48,9 +48,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, z, l, o]) => {
diff --git a/tools/feed-switch-seed-test.js b/tools/feed-switch-seed-test.js
index bf2b830..6d9d4f0 100644
--- a/tools/feed-switch-seed-test.js
+++ b/tools/feed-switch-seed-test.js
@@ -3,7 +3,7 @@
// other open feeds already hold, and once the feed on screen settles the
// other feeds' first posts are fetched ahead into the disk cache.
// Run: bun tools/feed-switch-seed-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -70,9 +70,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, a, b, ns]) => {
diff --git a/tools/feed-video-aspect-test.js b/tools/feed-video-aspect-test.js
index 84c0568..4196fe1 100644
--- a/tools/feed-video-aspect-test.js
+++ b/tools/feed-video-aspect-test.js
@@ -4,7 +4,7 @@
// shape still gets the 16:9 box.
// Photos too: a big one paints from a column-sized copy (see makeMediaThumb).
// Run: bun tools/feed-video-aspect-test.js (CHROME=<path> to pick a browser)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -96,9 +96,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const key = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-video-pager-test.js b/tools/feed-video-pager-test.js
index 61119a0..5bba7d8 100644
--- a/tools/feed-video-pager-test.js
+++ b/tools/feed-video-pager-test.js
@@ -4,7 +4,7 @@
// Each clip has a seek bar to drag; a tap pauses (and shows the details),
// another plays on. Leaving full screen leaves the pager.
// Run: bun tools/feed-video-pager-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -82,9 +82,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/feed-window-dom-test.js b/tools/feed-window-dom-test.js
index c86e98e..1f6ff93 100644
--- a/tools/feed-window-dom-test.js
+++ b/tools/feed-window-dom-test.js
@@ -3,7 +3,7 @@
// page keeps its length, the row under your eyes never moves when the window
// shifts, and scrolling back up brings the top posts back.
// Run: bun tools/feed-window-dom-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -78,9 +78,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/fullscreen-notice-test.js b/tools/fullscreen-notice-test.js
index d3b2785..a7c041a 100644
--- a/tools/fullscreen-notice-test.js
+++ b/tools/fullscreen-notice-test.js
@@ -5,7 +5,7 @@
// (feed-video-pager-test). A feed clip's own bar has no full-screen button:
// a tap on the clip is the way, into the pager.
// Run: bun tools/fullscreen-notice-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -79,9 +79,9 @@ async function run(lockOk) {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/gif-picker-test.js b/tools/gif-picker-test.js
index f72cde3..9a48de9 100644
--- a/tools/gif-picker-test.js
+++ b/tools/gif-picker-test.js
@@ -4,7 +4,7 @@
// In a DM the picked GIF is sent at once. Uses the live API โ€” localhost is
// admitted as nostr.build's shared development client.
// Run: bun tools/gif-picker-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -35,9 +35,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk]) => {
diff --git a/tools/go-live-test.js b/tools/go-live-test.js
index c3fc8e3..3990fa6 100644
--- a/tools/go-live-test.js
+++ b/tools/go-live-test.js
@@ -6,7 +6,7 @@
// recording, and the recording is complete.
// Run: bun tools/go-live-test.js (LIVE_SERVER=path to coinosv3-server/live/server.js; NO_BUILD=1 to reuse dist/;
// LIVE_CONFIG=a json with {sfuAppId, sfuSecret} to watch over WebRTC โ€” the real Cloudflare SFU)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { naddrEncode } from 'nostr-tools/nip19';
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
@@ -129,9 +129,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
const base = cacheKeyFor(mn + '\n');
await page.evaluate((k) => {
diff --git a/tools/grid-early-player-test.js b/tools/grid-early-player-test.js
index 0ab1c1e..04e44fb 100644
--- a/tools/grid-early-player-test.js
+++ b/tools/grid-early-player-test.js
@@ -14,7 +14,7 @@
// that); on a link slower than the clip, more โ€” to the 4 MB cap. With Data
// Saver on, the pager is careful again: such a clip waits for a tap and
// nothing loads ahead.
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -100,9 +100,9 @@ async function run() {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/history-export-test.js b/tools/history-export-test.js
index f7fff82..877b95e 100644
--- a/tools/history-export-test.js
+++ b/tools/history-export-test.js
@@ -2,7 +2,7 @@
// movements) as a line of a CSV file, oldest first, with what was paid, the
// fee beside it and the change to the balance; a failed payment is left out.
// Run: bun tools/history-export-test.js (NO_BUILD=1 to reuse dist/)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
@@ -43,9 +43,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
// Spending's history, as the Ark manager stores it (moved into IndexedDB at the next boot)
const key = cacheKeyFor(mn + '\n') + ':ark:' + ns('http://localhost:3535');
diff --git a/tools/image-tap-test.js b/tools/image-tap-test.js
index d3bcb7e..ce40d53 100644
--- a/tools/image-tap-test.js
+++ b/tools/image-tap-test.js
@@ -3,7 +3,7 @@
// the picture opens the picture.
//
// Run: bun tools/image-tap-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -27,9 +27,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const A = 'a'.repeat(63) + '9';
diff --git a/tools/lightbox-scroll-test.js b/tools/lightbox-scroll-test.js
index be03e9b..f507557 100644
--- a/tools/lightbox-scroll-test.js
+++ b/tools/lightbox-scroll-test.js
@@ -6,7 +6,7 @@
// thread had been opened from, or to the top. Back out of a thread still
// lands on the row that opened it.
// Run: bun tools/lightbox-scroll-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -56,9 +56,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/link-preview-test.js b/tools/link-preview-test.js
index 52ddca1..190b220 100644
--- a/tools/link-preview-test.js
+++ b/tools/link-preview-test.js
@@ -4,7 +4,7 @@
// nothing to say stays a bare link; pictures and YouTube keep their own
// boxes.
// Run: bun tools/link-preview-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -80,9 +80,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
// follow the author, so the feed asks for their posts
diff --git a/tools/live-stream-test.js b/tools/live-stream-test.js
index 2be6051..56dcb71 100644
--- a/tools/live-stream-test.js
+++ b/tools/live-stream-test.js
@@ -6,7 +6,7 @@
// profile says they're on, Back stops the player, the browser without HLS
// of its own plays it through hls.js, and a zap carries the stream's `a`.
// Run: bun tools/live-stream-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { naddrEncode } from 'nostr-tools/nip19';
@@ -150,9 +150,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/lnurl-withdraw-links-test.js b/tools/lnurl-withdraw-links-test.js
new file mode 100644
index 0000000..b349e17
--- /dev/null
+++ b/tools/lnurl-withdraw-links-test.js
@@ -0,0 +1,166 @@
+// Prepaid withdraw links (src/features/withdraw-links.js + coinosv3-server
+// names/withdraw.js): create one from Send (it pays the registrar up front),
+// get an LNURL QR, have it claimed โ€” here by our own LNURL-withdraw redeem
+// flow (zaps.js), the same road any Lightning wallet takes โ€” and take back
+// what's left. The registrar is the real withdraw service behind a stand-in
+// HTTP server (NIP-98 checked) with a fake Lightning node; the Ark side is
+// faked through the hooks. Run: bun tools/lnurl-withdraw-links-test.js
+import assert from 'node:assert/strict';
+import { verifyEvent } from 'nostr-tools/pure';
+import puppeteer from './puppeteer.js';
+import { withdrawService } from '../../coinosv3-server/names/withdraw.js';
+
+const app = await Bun.file('src/app.js').text();
+const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
+const entry = `
+import { withdrawLinks } from './src/features/withdraw-links.js';
+import { zapsFeature } from './src/features/zaps.js';
+import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
+${domHelper}
+const sk = generateSecretKey();
+const ui = { screen: 'wallet', tab: 'send', giftMode: false, send: { recipients: [{ address: '', amount: '' }] } };
+let lnInvoices = 0; const watched = [];
+const wallet = { watchOnly: false, nostrSign: async (e) => finalizeEvent(e, sk), loadFeatureState: (k, d) => d, saveFeatureState: () => {} };
+const api = async (p) => (await fetch(p)).json();
+const ctx = {
+ h, ui, wallet, render, toast: (m) => (window.toasts = (window.toasts || [])).push(m),
+ withdrawRegistrar: location.origin,
+ copyBtn: (v, label) => h('button', { class: 'btn-sm', onClick: () => { window.copied = v; } }, label),
+ goBack: (fn) => { fn(); render(); },
+ blankSend: () => ({ recipients: [{ address: '', amount: '' }] }),
+ fmtAmount: (n) => Number(n).toLocaleString('en-US'), unitLabel: () => 'sats', unitTag: () => h('span', { class: 'unit-tag' }, 'sats'),
+ parseAmount: (v) => { const n = Number(String(v).replace(/,/g, '')); return isFinite(n) && n > 0 ? Math.round(n) : null; }, getUnit: () => 'sats',
+ amountField: (sat) => String(sat),
+ hook(name, ...a) {
+ if (name === 'arkReady' || name === 'canLnPay') return true;
+ if (name === 'arkSpendableSat') return window.spendable ?? 100000;
+ if (name === 'arkPayInvoice') { window.paidInvoices = (window.paidInvoices || 0) + 1; return api('/__pay?bolt11=' + a[0]); }
+ // a coinos wallet's Lightning receive lives on the registrar's own node
+ if (name === 'arkLnInvoice') { lnInvoices++; return api('/__mkinv?own=1&sat=' + a[0]).then((r) => ({ id: 'ln' + lnInvoices, invoice: r.bolt11, amountSat: a[0] })); }
+ if (name === 'arkLnWatch') { watched.push(a[0]); return true; }
+ if (name === 'arkLnUnwatch' || name === 'arkLnCancel') return true;
+ return null;
+ },
+};
+const wl = withdrawLinks(ctx);
+const zaps = zapsFeature(ctx);
+function render() {
+ const v = (ui.giftMode === 'lnurl' && wl.view()) || zaps.sendView() || h('div', { id: 'closed' }, wl.entry());
+ document.querySelector('#app').replaceChildren(v);
+}
+window.test = { ui, render, pk: getPublicKey(sk), redeem: (s) => zaps.matchSendText(s, false), watched };
+render();
+`;
+const bundle = await Bun.build({
+ entrypoints: ['wl-test-entry'], target: 'browser',
+ plugins: [{ name: 'wl-test', setup(build) {
+ build.onResolve({ filter: /^wl-test-entry$/ }, () => ({ path: 'entry', namespace: 'wl-test' }));
+ build.onLoad({ filter: /.*/, namespace: 'wl-test' }, () => ({ contents: entry, loader: 'js', resolveDir: process.cwd() }));
+ } }],
+});
+assert(bundle.success, bundle.logs.join('\n'));
+const css = await Bun.file('src/style.css').text();
+const js = await bundle.outputs[0].text();
+const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style>${css}</style><div id="app"></div><script type="module">${js}</script>`;
+
+// ---- the stand-in registrar: the real service, a fake node ----
+const OUR = '02' + 'aa'.repeat(32), THEIRS = '03' + 'bb'.repeat(32);
+const invoices = {}, decoded = {}, outbound = {};
+let seq = 0, circular = 0;
+const mk = (sat, payee) => { const hash = (++seq).toString(16).padStart(64, '0'); const b = 'lnbc' + sat + 'n1fake' + seq; decoded[b] = { valid: true, payment_hash: hash, amount_msat: sat * 1000, payee, created_at: Math.floor(Date.now() / 1000), expiry: 3600 }; return { b, hash }; };
+const ln = { async call(m, p) {
+ if (m === 'invoice') { const { b, hash } = mk(p.amount_msat / 1000, OUR); invoices[hash] = { status: 'unpaid', bolt11: b }; return { bolt11: b, payment_hash: hash }; }
+ if (m === 'listinvoices') return { invoices: invoices[p.payment_hash] ? [invoices[p.payment_hash]] : [] };
+ if (m === 'decode') { if (!decoded[p.string]) throw new Error('bad'); return decoded[p.string]; }
+ if (m === 'pay') { const d = decoded[p.bolt11]; outbound[d.payment_hash] = 'complete'; return { status: 'complete', amount_msat: d.amount_msat, amount_sent_msat: d.amount_msat }; }
+ if (m === 'waitsendpay') { outbound[p.payment_hash] = 'complete'; return { status: 'complete' }; }
+ if (m === 'listsendpays') return { payments: outbound[p.payment_hash] ? [{ status: outbound[p.payment_hash] }] : [] };
+ throw new Error('unexpected ' + m);
+} };
+const state = {};
+let svc; // made once the server has its port: the service's URLs are built on it
+const nip98 = async (req, url, body) => {
+ const evt = JSON.parse(atob((req.headers.get('authorization') || '').replace(/^Nostr /, '')));
+ assert(verifyEvent(evt) && evt.kind === 27235 && new URL(evt.tags.find((t) => t[0] === 'u')[1]).pathname === url.pathname);
+ return evt.pubkey;
+};
+const server = Bun.serve({ port: 0, async fetch(req) {
+ const u = new URL(req.url);
+ const j = (b, s = 200) => Response.json(b, { status: s });
+ if (u.pathname === '/__pay') { const d = decoded[u.searchParams.get('bolt11')]; invoices[d.payment_hash].status = 'paid'; return j({ ok: true }); }
+ if (u.pathname === '/__mkinv') { const { b } = mk(Number(u.searchParams.get('sat')), u.searchParams.get('own') ? OUR : THEIRS); return j({ bolt11: b }); }
+ const w = u.pathname.match(/^\/w\/([A-Za-z0-9_-]+)(\/cb)?$/);
+ if (w) return j(w[2] ? await svc.callback(w[1], u.searchParams.get('k1'), u.searchParams.get('pr')) : svc.params(w[1]));
+ if (u.pathname.startsWith('/withdraw/')) {
+ const body = req.method === 'POST' ? await req.text() : '';
+ const pk = await nip98(req, u, body);
+ const b = body ? JSON.parse(body) : {};
+ try {
+ if (u.pathname === '/withdraw/mine') return j({ links: svc.mine(pk) });
+ if (u.pathname === '/withdraw/new') return j(await svc.create({ pubkey: pk, ...b }));
+ if (u.pathname === '/withdraw/funded') return j(await svc.confirmFunded(b.id, pk));
+ if (u.pathname === '/withdraw/reclaim') return j(await svc.reclaim(b.id, pk, b.invoice));
+ } catch (e) { return j({ error: e.message }, e.status || 500); }
+ }
+ return new Response(html, { headers: { 'content-type': 'text/html' } });
+} });
+svc = withdrawService({ state, persist: () => {}, lnFor: () => ln, ourNodeId: async () => OUR,
+ circularPick: async () => ({ cap: 1e9 }), payCircular: async () => { circular++; return { feeMsat: 0 }; }, publicBase: server.url.origin.replace(/\/$/, '') });
+
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const errors = [];
+page.on('pageerror', (e) => errors.push(e.message));
+const pause = (ms) => new Promise((r) => setTimeout(r, ms));
+const check = (name, value, detail = '') => { assert(value, name + (detail ? ' โ€” ' + detail : '')); console.log(' โœ“ ' + name); };
+const text = () => page.evaluate(() => document.body.innerText.replace(/\s+/g, ' '));
+const waitText = async (re, ms = 6000) => { for (let i = 0; i < ms / 100; i++) { if (re.test(await text())) return true; await pause(100); } return false; };
+const click = (label) => page.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().startsWith(x)); if (!b) return false; b.click(); return true; }, label);
+const type = (sel, v) => page.evaluate(([s, val]) => { const i = document.querySelector(s); i.value = val; i.dispatchEvent(new Event('input', { bubbles: true })); }, [sel, v]);
+try {
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto(server.url.href);
+ await page.waitForSelector('#wl-open');
+ console.log('[making a link]');
+ await page.click('#wl-open');
+ check('Send opens the withdraw-link page', await waitText(/A Lightning link or QR code that anyone can withdraw sats from/));
+ await page.click('#wl-create');
+ check('no amount: asked for one, nothing paid', await waitText(/Enter an amount/) && !(await page.evaluate(() => window.paidInvoices)));
+ await type('#wl-amount', '500'); await type('#wl-uses', '3');
+ check('the button and the line say what gets paid now', /Create link for 1,500 sats/.test(await text()) && /1,500 sats paid now from Spending: 3 withdrawals of up to 500 sats/.test(await text()), await text());
+ await page.evaluate(() => { window.spendable = 1000; });
+ await page.click('#wl-create');
+ check('more than Spending holds: refused before paying', await waitText(/Not enough in Spending for 1,500 sats/) && !(await page.evaluate(() => window.paidInvoices)));
+ await page.evaluate(() => { window.spendable = 100000; });
+ await type('textarea', 'Meetup door prize');
+ await page.click('#wl-create');
+ check('it pays the registrar once and shows the link', await waitText(/Your withdraw link is ready/) && (await page.evaluate(() => window.paidInvoices)) === 1, await text());
+ check('โ€ฆwith a QR, the terms and the note', !!(await page.$('svg')) && /Up to 500 sats each, 3 of 3 withdrawals left/.test(await text()) && /Meetup door prize/.test(await text()), await text());
+ const lnurl = await page.evaluate(() => [...document.querySelectorAll('.addr-box')].map((e) => e.textContent.trim())[0]);
+ check('the link is an LNURL', /^LNURL1[0-9A-Z]+$/.test(lnurl), lnurl);
+ const link = Object.values(state.withdraws)[0];
+ check('the registrar holds it funded', link.status === 'open' && link.totalSat === 1500 && link.pubkey === (await page.evaluate(() => test.pk)));
+
+ console.log('\n[claiming it with a Lightning wallet: ours]');
+ await click('Back');
+ await page.evaluate(() => { test.ui.giftMode = false; });
+ check('the redeem flow takes the LNURL', await page.evaluate((s) => test.redeem(s), lnurl));
+ check('โ€ฆand reads the link\'s terms', await waitText(/will pay you between 1 and 500 sats/) && /Meetup door prize/.test(await text()), await text());
+ await click('Withdraw');
+ check('the claim is paid out of the link', await waitText(/has your invoice for 500 sats/) && (await (async () => { await pause(100); return link.claims[0]?.status === 'paid'; })()), JSON.stringify(link.claims));
+ check('โ€ฆround the circular route (a coinos wallet\'s own invoice)', circular === 1);
+
+ console.log('\n[taking back the rest]');
+ await page.evaluate(() => { test.ui.lnWithdraw = null; test.ui.giftMode = false; test.render(); });
+ await page.evaluate(() => document.querySelector('#wl-open').click());
+ check('the list shows what is left', await waitText(/Your withdraw links/) && /1,000 sats left/.test(await text()), await text());
+ await page.evaluate(() => document.querySelector('.wl-row').click());
+ check('the link page shows 2 of 3 left', await waitText(/2 of 3 withdrawals left/));
+ await click('Take back 1,000 sats');
+ check('take back pays the remainder to the wallet and closes the link', await waitText(/This link is closed/) && link.status === 'closed' && link.spentSat === 1500, JSON.stringify(link));
+ check('โ€ฆand the wallet watched for that payment', (await page.evaluate(() => test.watched.length)) >= 1);
+ check('โ€ฆthe toast says so', (await page.evaluate(() => window.toasts || [])).some((m) => /1,000 sats is back in Spending/.test(m)));
+ check('a closed link refuses claims', svc.params(link.id).status === 'ERROR');
+ check('no page errors', errors.length === 0, errors.join('; '));
+ console.log('\nโœ… prepaid withdraw links');
+} catch (e) { console.error('\nโŒ', e.message); process.exitCode = 1; } finally { await browser.close(); server.stop(true); }
diff --git a/tools/media-grid-dups-test.js b/tools/media-grid-dups-test.js
index e7b67da..1a00222 100644
--- a/tools/media-grid-dups-test.js
+++ b/tools/media-grid-dups-test.js
@@ -5,7 +5,7 @@
// that won't load leaves the Images grid instead of a broken-picture glyph. And on a big screen with a mouse, a
// small clip opened in the pager is drawn at most 1.5ร— its own pixels, not
// blown up across the monitor. Run: bun tools/media-grid-dups-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -92,9 +92,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/media-grid-order-test.js b/tools/media-grid-order-test.js
index 9274d92..0e333f0 100644
--- a/tools/media-grid-order-test.js
+++ b/tools/media-grid-order-test.js
@@ -3,7 +3,7 @@
// jumped around): tiles found later go under the ones already showing, in
// their own date order, even when they are dated between them; only a post
// newer than the top tile goes above it. Run: bun tools/media-grid-order-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -97,9 +97,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const key = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk]) => {
diff --git a/tools/media-moments-feed-test.js b/tools/media-moments-feed-test.js
index 7b429c9..5b4c8fb 100644
--- a/tools/media-moments-feed-test.js
+++ b/tools/media-moments-feed-test.js
@@ -1,6 +1,6 @@
// Full feed integration with local media and relay acknowledgements; no events leave the test.
// Run: bun tools/media-moments-feed-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -111,9 +111,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const key = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/mention-ac-test.js b/tools/mention-ac-test.js
index 52ba1ef..e476e61 100644
--- a/tools/mention-ac-test.js
+++ b/tools/mention-ac-test.js
@@ -2,7 +2,7 @@
// people โ€” the ones you follow first, then the search โ€” and picking one puts
// nostr:npubโ€ฆ in the text; the post goes out with their p tag.
// Run: bun tools/mention-ac-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
@@ -59,9 +59,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await sleep(3000); // a new wallet's starter follows land first; ours go over them
const base = cacheKeyFor(mn + '\n');
diff --git a/tools/moderation-test.js b/tools/moderation-test.js
index 426115c..8856864 100644
--- a/tools/moderation-test.js
+++ b/tools/moderation-test.js
@@ -5,7 +5,7 @@
// are hidden; Settings โ†’ Nostr lays the mute list out and edits it.
// No relay is written for real โ€” the WebSocket shim swallows EVENT.
// Run: bun tools/moderation-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -114,9 +114,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME_SK = privateKeyFromSeedWords(mn);
diff --git a/tools/negentropy-sync-test.js b/tools/negentropy-sync-test.js
index e0257ca..c7adb84 100644
--- a/tools/negentropy-sync-test.js
+++ b/tools/negentropy-sync-test.js
@@ -8,7 +8,7 @@
// backlog, and is remembered as such for a day: the next ask there goes
// straight out as a REQ.
// Run: bun tools/negentropy-sync-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { buildHtml } from '../build.js';
@@ -48,9 +48,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const ME = getPublicKey(privateKeyFromSeedWords(mn));
const MINE = finalizeEvent({ kind: 1, created_at: now - 600, tags: [], content: 'my post, replied to' }, FRIEND.sk);
diff --git a/tools/notif-categories-test.js b/tools/notif-categories-test.js
index f38dd01..8bd0da1 100644
--- a/tools/notif-categories-test.js
+++ b/tools/notif-categories-test.js
@@ -1,7 +1,7 @@
// Settings โ†’ Notifications lists zaps and reactions as their own categories,
// each with its own switch. Run: bun tools/notif-categories-test.js
import assert from 'node:assert/strict';
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const html = await buildHtml({ minify: true, pwa: false });
@@ -19,7 +19,7 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload(); await sleep(500);
await click('create a new wallet'); await sleep(600);
- await click('skip verification'); await sleep(300);
+ await skipSeedCheck(page); await sleep(300);
await page.waitForFunction(() => document.body.innerText.toLowerCase().includes('receive'), { timeout: 20000 });
await page.evaluate(() => [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings').click()); await sleep(600);
await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find((e) => /notification/i.test(e.textContent)).click()); await sleep(600);
diff --git a/tools/notif-custom-emoji-test.js b/tools/notif-custom-emoji-test.js
index 5847a0c..b1ff564 100644
--- a/tools/notif-custom-emoji-test.js
+++ b/tools/notif-custom-emoji-test.js
@@ -3,7 +3,7 @@
// :peepoClap: text, because the row kept the content and not the tag.
// A row saved before that is healed when the relay hands the reaction again.
// Run: bun tools/notif-custom-emoji-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -34,9 +34,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME = getPublicKey(privateKeyFromSeedWords(mn));
diff --git a/tools/notif-faces-test.js b/tools/notif-faces-test.js
index a1c1c8f..0aa7a92 100644
--- a/tools/notif-faces-test.js
+++ b/tools/notif-faces-test.js
@@ -3,7 +3,7 @@
// opens with faces, not circles filling in. The stub relay answers the kind-0
// batch for the actors with pictures served by this test.
// Run: bun tools/notif-faces-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -65,9 +65,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME = getPublicKey(privateKeyFromSeedWords(mn));
diff --git a/tools/notif-note-cache-test.js b/tools/notif-note-cache-test.js
index e24c2e2..2b8be67 100644
--- a/tools/notif-note-cache-test.js
+++ b/tools/notif-note-cache-test.js
@@ -1,7 +1,7 @@
// Referenced notification notes are fetched before the page opens, persisted,
// and available on the first paint after a reload.
// Run: bun tools/notif-note-cache-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -84,9 +84,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await clickText('create a new wallet');
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mnemonic = (await page.$$eval('.words .w .t', (nodes) => nodes.map((node) => node.textContent.trim()))).join(' ');
- await clickText('skip verification');
+ await skipSeedCheck(page);
await waitText('receive');
const base = cacheKeyFor(mnemonic + '\n');
const me = getPublicKey(privateKeyFromSeedWords(mnemonic));
diff --git a/tools/notif-report-fold-test.js b/tools/notif-report-fold-test.js
index 678aefd..6a906ec 100644
--- a/tools/notif-report-fold-test.js
+++ b/tools/notif-report-fold-test.js
@@ -4,7 +4,7 @@
// (Adam's phone, 2026-10-14: a reply Amethyst showed was missing here). A
// person one follow flagged, or a stranger nobody did, shows as ever.
// Run: bun tools/notif-report-fold-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -36,9 +36,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME = getPublicKey(privateKeyFromSeedWords(mn));
diff --git a/tools/nwc-settings-ui-test.js b/tools/nwc-settings-ui-test.js
index b88f9f7..ea52a6c 100644
--- a/tools/nwc-settings-ui-test.js
+++ b/tools/nwc-settings-ui-test.js
@@ -2,7 +2,7 @@
// budget that resets every day, week or month; the card says so, and
// Revoke keeps it (greyed out of the list) answering for a while.
// Run: bun tools/nwc-settings-ui-test.js (needs the regtest ark stack on :3535 for the card to show)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
@@ -47,8 +47,8 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings'); b.click(); }); await sleep(600);
await page.evaluate(() => { const b = [...document.querySelectorAll('.settings-tile')].find((e) => [...e.querySelectorAll('span')].some((x) => x.textContent.trim() === 'Nostr')); if (b) b.click(); }); await sleep(800);
diff --git a/tools/onboard-cover-test.js b/tools/onboard-cover-test.js
index 4262bc1..733a515 100644
--- a/tools/onboard-cover-test.js
+++ b/tools/onboard-cover-test.js
@@ -3,7 +3,7 @@
// twelve nature covers with a counter; the preview copies are what load.
// Run: bun run build && bun tools/onboard-cover-test.js [screenshot.png]
import assert from 'node:assert/strict';
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const server = Bun.serve({ port: 0, async fetch(req) {
const p = new URL(req.url).pathname;
@@ -21,7 +21,7 @@ try {
await page.reload(); await sleep(1200);
const click = (x) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); e?.click(); return !!e; }, x);
await click('create a new wallet'); await sleep(1500);
- await click('skip verification'); await sleep(1500);
+ await skipSeedCheck(page); await sleep(1500);
await page.evaluate(() => localStorage.setItem('btc-wallet-onb-step', 'avatar'));
await page.reload();
await page.waitForSelector('.onb-cover', { timeout: 10000 });
diff --git a/tools/own-post-feeds-test.js b/tools/own-post-feeds-test.js
index a3e8fc3..a877749 100644
--- a/tools/own-post-feeds-test.js
+++ b/tools/own-post-feeds-test.js
@@ -6,7 +6,7 @@
// whose tag it wears, which is now a t tag on the event) โ€” not to a topic
// feed that happens to be open and doesn't match.
// Run: bun tools/own-post-feeds-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
@@ -63,9 +63,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME = getPublicKey(privateKeyFromSeedWords(mn));
diff --git a/tools/pager-caption-test.js b/tools/pager-caption-test.js
index 50ffe4f..a3d7800 100644
--- a/tools/pager-caption-test.js
+++ b/tools/pager-caption-test.js
@@ -4,7 +4,7 @@
// caption leaves the pager and opens the post, where the whole caption
// reads. A link to the web opens in its own tab and the pager stays.
// Run: bun tools/pager-caption-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
@@ -87,9 +87,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, valley, ns]) => {
diff --git a/tools/pager-close-test.js b/tools/pager-close-test.js
index a7c6c12..2f0bece 100644
--- a/tools/pager-close-test.js
+++ b/tools/pager-close-test.js
@@ -6,7 +6,7 @@
// the reader on that page, and that entry becomes it. And the ordinary case
// still steps Back, so the phone's Back doesn't reopen the pager.
// Run: bun tools/pager-close-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -96,9 +96,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-controls-test.js b/tools/pager-controls-test.js
index a02591d..79e9bca 100644
--- a/tools/pager-controls-test.js
+++ b/tools/pager-controls-test.js
@@ -4,7 +4,7 @@
// the reaction button offers the emoji sheet, over the pager, as the feed's
// does; the emoji picked shows on the button, and a tap again takes it back.
// Run: bun tools/pager-controls-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -54,9 +54,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-eager-test.js b/tools/pager-eager-test.js
index 3d980fb..f1cd150 100644
--- a/tools/pager-eager-test.js
+++ b/tools/pager-eager-test.js
@@ -8,7 +8,7 @@
// Saver on, the pager is careful again: such a clip waits for a tap and
// nothing loads ahead.
// Run: bun tools/pager-eager-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -93,9 +93,9 @@ async function run(saveData, slow = false) {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-first-frame-test.js b/tools/pager-first-frame-test.js
index 0d477b7..63de5ac 100644
--- a/tools/pager-first-frame-test.js
+++ b/tools/pager-first-frame-test.js
@@ -5,7 +5,7 @@
// opening, and a clip beyond gets no picture (and stays out of the
// carousel) until a first frame comes โ€” never the post's still first. The slide left behind keeps its frame for the swipe
// back. Run: bun tools/pager-first-frame-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -81,9 +81,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-gone-picture-test.js b/tools/pager-gone-picture-test.js
index 34c89b6..b7e3c8f 100644
--- a/tools/pager-gone-picture-test.js
+++ b/tools/pager-gone-picture-test.js
@@ -2,7 +2,7 @@
// nor the original can load) never joins the pager's carousel: a picture
// joins once decoded (2026-10-17), and a step passes over it to the next.
// (Its slide is marked gone, with the host's name, as before โ€” out of sight.) Run: bun tools/pager-gone-picture-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -43,9 +43,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-no-poster-test.js b/tools/pager-no-poster-test.js
index 332eb53..c40f73e 100644
--- a/tools/pager-no-poster-test.js
+++ b/tools/pager-no-poster-test.js
@@ -7,7 +7,7 @@
// connection to be careful with (Data Saver), where a clip may never load,
// the thumbnail poster stays.
// Run: bun tools/pager-no-poster-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -77,9 +77,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-prime-test.js b/tools/pager-prime-test.js
index 4989790..df20cc7 100644
--- a/tools/pager-prime-test.js
+++ b/tools/pager-prime-test.js
@@ -15,7 +15,7 @@
// that); on a link slower than the clip, more โ€” to the 4 MB cap. With Data
// Saver on, the pager is careful again: such a clip waits for a tap and
// nothing loads ahead.
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -97,9 +97,9 @@ async function run() {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-ready-gate-test.js b/tools/pager-ready-gate-test.js
index dd6982d..20cb337 100644
--- a/tools/pager-ready-gate-test.js
+++ b/tools/pager-ready-gate-test.js
@@ -5,7 +5,7 @@
// grid โ€” shows the clips the screen's way round (tall ones on a phone held
// upright), a square either way, until the toggle asks for all shapes.
// Run: bun tools/pager-ready-gate-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -81,9 +81,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-resize-test.js b/tools/pager-resize-test.js
index 07fcbba..d18745b 100644
--- a/tools/pager-resize-test.js
+++ b/tools/pager-resize-test.js
@@ -5,7 +5,7 @@
// button on a clip 23 posts down showed him another post's picture. The
// resize can come before the observer has even named the first slide.
// Run: bun tools/pager-resize-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -68,9 +68,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-rotate-test.js b/tools/pager-rotate-test.js
index 9dcc7e3..4d453ad 100644
--- a/tools/pager-rotate-test.js
+++ b/tools/pager-rotate-test.js
@@ -4,7 +4,7 @@
// autoplay used to start it again, from the start, with sound if it had been
// unmuted there), and not the slide a turn's half-way layout passes over.
// Run: bun tools/pager-rotate-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -68,9 +68,9 @@ async function run(unmuteFirst) {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-seek-hide-test.js b/tools/pager-seek-hide-test.js
index 50863ba..59a0353 100644
--- a/tools/pager-seek-hide-test.js
+++ b/tools/pager-seek-hide-test.js
@@ -8,7 +8,7 @@
// position counter is the other way
// round: it shows while the pager moves and fades out once it has settled.
// Run: bun tools/pager-seek-hide-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -70,9 +70,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-sharpen-test.js b/tools/pager-sharpen-test.js
index ea5c878..e61b51d 100644
--- a/tools/pager-sharpen-test.js
+++ b/tools/pager-sharpen-test.js
@@ -3,7 +3,7 @@
// comes to rest; only the slide at rest carries the zoom layer. (Originals
// decoded and uploaded as they passed made a flight stutter โ€” Adam,
// 2026-10-16.) Run: bun tools/pager-sharpen-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -42,9 +42,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js
index 657ea75..1184e7b 100644
--- a/tools/pager-swipe-fly-test.js
+++ b/tools/pager-swipe-fly-test.js
@@ -5,7 +5,7 @@
// mid-flight stops it on the post most on screen, and a swipe back from
// there goes to the one before.
// Run: bun tools/pager-swipe-fly-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -85,9 +85,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-tail-test.js b/tools/pager-tail-test.js
index 9d537e1..96227f8 100644
--- a/tools/pager-tail-test.js
+++ b/tools/pager-tail-test.js
@@ -4,7 +4,7 @@
// moved onto and plays by itself. With nothing older to be had, the end says
// that's everything. Escape from there lands on the last clip's post.
// Run: bun tools/pager-tail-test.js (SHOT=path.png saves the loading screen)
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -84,9 +84,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-tap-slop-test.js b/tools/pager-tap-slop-test.js
index 4de87df..c184e3b 100644
--- a/tools/pager-tap-slop-test.js
+++ b/tools/pager-tap-slop-test.js
@@ -4,7 +4,7 @@
// 30px pauses (or plays) the clip and leaves the pager where it was; only a
// longer one pages.
// Run: bun tools/pager-tap-slop-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -66,9 +66,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pager-wheel-test.js b/tools/pager-wheel-test.js
index 45a5115..7f59c34 100644
--- a/tools/pager-wheel-test.js
+++ b/tools/pager-wheel-test.js
@@ -5,7 +5,7 @@
// the wheel pans it while zoomed and pages again at 1ร—; and arrows at the
// edges for the pointer, the sideways pair only where the post has more.
// Run: bun tools/pager-wheel-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -68,9 +68,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/pic-proxy-test.js b/tools/pic-proxy-test.js
index 881867c..69a09c1 100644
--- a/tools/pic-proxy-test.js
+++ b/tools/pic-proxy-test.js
@@ -5,7 +5,7 @@
// paints the original as before. The server is stood in for: its answers are
// canned.
// Run: bun tools/pic-proxy-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -76,9 +76,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/picture-pager-ready-test.js b/tools/picture-pager-ready-test.js
index 7cfaa45..08a4e21 100644
--- a/tools/picture-pager-ready-test.js
+++ b/tools/picture-pager-ready-test.js
@@ -5,7 +5,7 @@
// the screen stay out, with the "Tall only" / "All shapes" toggle in the
// top bar; and with fewer than ten ready and more still warming, no
// counter shows and a flick goes one post. Run: bun tools/picture-pager-ready-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -50,9 +50,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/poll-test.js b/tools/poll-test.js
index edcb061..41aa02e 100644
--- a/tools/poll-test.js
+++ b/tools/poll-test.js
@@ -4,7 +4,7 @@
// caught at the socket so the test never votes on someone's real poll.
//
// Run: bun tools/poll-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { SimplePool } from 'nostr-tools/pool';
import { cacheKeyFor } from '../src/wallet.js';
import { neventOf } from '../src/nostr.js';
@@ -41,9 +41,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const Q = 'a'.repeat(63) + '9';
diff --git a/tools/post-edit-test.js b/tools/post-edit-test.js
index 2ed1260..08d1190 100644
--- a/tools/post-edit-test.js
+++ b/tools/post-edit-test.js
@@ -5,7 +5,7 @@
// deletion is remembered across a reload.
// No relay is written for real โ€” the WebSocket shim swallows EVENT.
// Run: bun tools/post-edit-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -95,9 +95,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const ME_SK = privateKeyFromSeedWords(mn);
diff --git a/tools/profile-pager-test.js b/tools/profile-pager-test.js
index 2a78598..5593192 100644
--- a/tools/profile-pager-test.js
+++ b/tools/profile-pager-test.js
@@ -3,7 +3,7 @@
// else's โ€” and closing it after moving on lands the profile on that post.
// (It opened alone, in the lightbox, before: no pager on a profile.)
// Run: bun tools/profile-pager-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -67,9 +67,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns, other]) => {
diff --git a/tools/profile-repost-test.js b/tools/profile-repost-test.js
index 7825efe..5ee97e7 100644
--- a/tools/profile-repost-test.js
+++ b/tools/profile-repost-test.js
@@ -4,7 +4,7 @@
// boost, a video event still has its clip โ€” they were all saved as kind 1
// notes, so a boost came back as "reply ยท {"content":โ€ฆ}".
// Run: bun tools/profile-repost-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
import { buildHtml } from '../build.js';
@@ -57,8 +57,8 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.goto(server.url.href + npubEncode(ME.pk), { waitUntil: 'domcontentloaded' });
await look('from the relays');
diff --git a/tools/puppeteer.js b/tools/puppeteer.js
index 2609981..8cefd6e 100644
--- a/tools/puppeteer.js
+++ b/tools/puppeteer.js
@@ -126,3 +126,20 @@ export default new Proxy(real, {
return typeof v === 'function' ? v.bind(t) : v;
},
});
+
+// The create flow (2026-11-07): a heads-up, then the words, then the
+// passphrase page, then a one-word check. seedWords() gets a test from
+// "Create a new wallet" to the words on screen; skipSeedCheck() from there
+// (or from the heads-up) into the wallet, no passphrase, check skipped.
+export async function seedWords(page) {
+ await page.waitForSelector('#seed-intro-next, .words .w .t', { timeout: 30000 });
+ if (await page.$('#seed-intro-next')) await page.evaluate(() => document.getElementById('seed-intro-next').click());
+ await page.waitForSelector('.words .w .t');
+}
+export async function skipSeedCheck(page) {
+ if (await page.$('#seed-intro-next')) await seedWords(page);
+ for (const id of ['seed-next', 'pass-next', 'seed-skip']) {
+ await page.waitForSelector('#' + id, { timeout: 10000 });
+ await page.evaluate((i) => document.getElementById(i).click(), id);
+ }
+}
diff --git a/tools/quote-pager-test.js b/tools/quote-pager-test.js
index 7f8b76c..d125edd 100644
--- a/tools/quote-pager-test.js
+++ b/tools/quote-pager-test.js
@@ -6,7 +6,7 @@
// quoted picture's details name its author, and closing lands on the post
// that quoted it.
// Run: bun tools/quote-pager-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { noteEncode } from 'nostr-tools/nip19';
@@ -80,9 +80,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pa, pq]) => {
diff --git a/tools/quoted-clip-fullscreen-test.js b/tools/quoted-clip-fullscreen-test.js
index 41984a2..ce235f6 100644
--- a/tools/quoted-clip-fullscreen-test.js
+++ b/tools/quoted-clip-fullscreen-test.js
@@ -5,7 +5,7 @@
// handler took the clip back out of full screen for a pager that then
// wouldn't open.
// Run: bun tools/quoted-clip-fullscreen-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -47,9 +47,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, alice, bob, roots, replies]) => {
diff --git a/tools/relay-budget-test.js b/tools/relay-budget-test.js
index bb1dd11..d9798a3 100644
--- a/tools/relay-budget-test.js
+++ b/tools/relay-budget-test.js
@@ -9,7 +9,7 @@
// unchanged live subscriptions stay up, the reports sync honours the saved
// clock, each relay's REQ closes on its own EOSE, and no relay carries more
// than ten one-shot REQs at once. Run: bun tools/relay-budget-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { neventEncode } from 'nostr-tools/nip19';
@@ -101,9 +101,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/relays-settings-test.js b/tools/relays-settings-test.js
index 9e18f59..771e3cd 100644
--- a/tools/relays-settings-test.js
+++ b/tools/relays-settings-test.js
@@ -3,7 +3,7 @@
// both only after a fresh lookup, newer than the copies it replaces.
// No relay is written for real โ€” the WebSocket shim swallows EVENT.
// Run: bun tools/relays-settings-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { buildHtml } from '../build.js';
@@ -71,9 +71,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const ME_SK = privateKeyFromSeedWords(mn);
const ME = getPublicKey(ME_SK);
diff --git a/tools/resume-wake-test.js b/tools/resume-wake-test.js
index 3b992fb..c142dfb 100644
--- a/tools/resume-wake-test.js
+++ b/tools/resume-wake-test.js
@@ -7,7 +7,7 @@
// 2026-11-01). On resume every open socket is now asked one tiny REQ; a
// silent one is closed and redialed at once, its subscriptions refired, and
// the feed refresh runs on the live sockets. Run: bun tools/resume-wake-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -71,9 +71,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/room-faces-test.js b/tools/room-faces-test.js
index cc4c6fe..ecbcc14 100644
--- a/tools/room-faces-test.js
+++ b/tools/room-faces-test.js
@@ -4,7 +4,7 @@
// profiles cached โ€” the worst case โ€” against the real coinos community.
//
// Run: bun tools/room-faces-test.js [--no-hover]
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
@@ -22,8 +22,8 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.evaluate(() => [...document.querySelectorAll('.app-bottom-nav .app-nav-button')][1].click());
await waitText('members', 20000);
diff --git a/tools/scheduled-post-test.js b/tools/scheduled-post-test.js
index 546537c..e4a61f1 100644
--- a/tools/scheduled-post-test.js
+++ b/tools/scheduled-post-test.js
@@ -4,7 +4,7 @@
// post waits in a "Scheduled" list, where Cancel sends a kind 5 for it to
// /schedule/cancel. The notifier is stubbed.
// Run: bun tools/scheduled-post-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { verifyEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -47,8 +47,8 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]')?.click());
diff --git a/tools/sensitive-feed-test.js b/tools/sensitive-feed-test.js
index a050269..626da2c 100644
--- a/tools/sensitive-feed-test.js
+++ b/tools/sensitive-feed-test.js
@@ -4,7 +4,7 @@
// accounts whose own profile says they post adult work. Elsewhere a marked
// post is folded behind its warning with a Show.
// Run: bun tools/sensitive-feed-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { buildHtml, buildVerifyWorker } from '../build.js';
@@ -74,9 +74,9 @@ try {
await sleep(400);
const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await page.waitForFunction(() => /receive/i.test(document.body.innerText), { timeout: 20000 });
await sleep(3000); // the starter follows land first
const base = cacheKeyFor(mn + '\n');
diff --git a/tools/signer-resume-test.js b/tools/signer-resume-test.js
index c35b655..9ca3950 100644
--- a/tools/signer-resume-test.js
+++ b/tools/signer-resume-test.js
@@ -6,7 +6,7 @@
// keeps trying the stored session and closes itself once the signer answers.
// The remote signer is played by this script over a shimmed relay.
// Run: bun tools/signer-resume-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import * as nip44 from 'nostr-tools/nip44';
@@ -100,9 +100,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const local = Array.from(CLIENT_SK, (b) => b.toString(16).padStart(2, '0')).join('');
diff --git a/tools/slow-image-test.js b/tools/slow-image-test.js
index 103f6da..43eff97 100644
--- a/tools/slow-image-test.js
+++ b/tools/slow-image-test.js
@@ -4,7 +4,7 @@
// inside a fixed box (nothing moves when it lands); a picture that refused to
// load is still a link.
// Run: bun tools/slow-image-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -46,9 +46,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/standalone-file-test.js b/tools/standalone-file-test.js
index 743bc7d..13445f6 100644
--- a/tools/standalone-file-test.js
+++ b/tools/standalone-file-test.js
@@ -3,7 +3,7 @@
// Run: bun run build && bun tools/standalone-file-test.js
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
@@ -25,7 +25,7 @@ try {
await page.evaluate(() => { const s = [...document.querySelectorAll('select')].find((x) => [...x.options].some((o) => o.value === 'en')); s.value = 'en'; s.dispatchEvent(new Event('change', { bubbles: true })); });
await sleep(500);
await click('create a new wallet'); await sleep(1500);
- await click('skip verification'); await sleep(1500);
+ await skipSeedCheck(page); await sleep(1500);
// the avatar step comes after a name claim; resume the wizard there
await page.evaluate(() => localStorage.setItem('btc-wallet-onb-step', 'avatar'));
await page.reload(); await sleep(2500);
diff --git a/tools/thread-instant-test.js b/tools/thread-instant-test.js
index bd80408..e9a9138 100644
--- a/tools/thread-instant-test.js
+++ b/tools/thread-instant-test.js
@@ -3,7 +3,7 @@
// is seeded from them โ€” the relays only add to it. A thread with nothing in
// hand paints its replies as they arrive, not after the slowest relay.
// Run: bun tools/thread-instant-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -95,9 +95,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, alice, bob, carol, notes]) => {
diff --git a/tools/translate-phone-lang-test.js b/tools/translate-phone-lang-test.js
index 9e1eefc..28676eb 100644
--- a/tools/translate-phone-lang-test.js
+++ b/tools/translate-phone-lang-test.js
@@ -6,7 +6,7 @@
// the target and takes a DeepL key, which then rides with each request.
// The registrar's /translate is stubbed (window.fetch shim).
// Run: bun tools/translate-phone-lang-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -57,9 +57,9 @@ try {
await page.evaluate(() => { localStorage.setItem('btc-wallet-network', 'regtest'); localStorage.setItem('btc-wallet-lang', 'en'); });
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/translate-test.js b/tools/translate-test.js
index d3fe095..f00ba22 100644
--- a/tools/translate-test.js
+++ b/tools/translate-test.js
@@ -3,7 +3,7 @@
// with the switch off they offer Translate instead; โ‹ฏ โ†’ Translate asks for
// any post. The registrar's /translate is stubbed here (window.fetch shim).
// Run: bun tools/translate-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -70,9 +70,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-autoplay-test.js b/tools/video-autoplay-test.js
index 6b943f2..6d245b3 100644
--- a/tools/video-autoplay-test.js
+++ b/tools/video-autoplay-test.js
@@ -4,7 +4,7 @@
// loaded for a box that never comes into view. Set CLIP to a webm to serve;
// the default is made with ffmpeg on the fly.
// Run: bun tools/video-autoplay-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -53,9 +53,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-compress-test.js b/tools/video-compress-test.js
index 19735c8..2b83edc 100644
--- a/tools/video-compress-test.js
+++ b/tools/video-compress-test.js
@@ -4,7 +4,7 @@
// upload is caught and its bytes checked with ffprobe.
//
// Run: bun tools/video-compress-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.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; };
@@ -73,8 +73,8 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(600);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
- await click('skip verification');
+ await seedWords(page);
+ await skipSeedCheck(page);
await waitText('receive', 20000);
check('the browser can encode H.264 (WebCodecs)', await page.evaluate(async () => (await VideoEncoder.isConfigSupported({ codec: 'avc1.42001f', width: 1280, height: 720, bitrate: 2.5e6 })).supported));
// the post composer: the pencil on the feed
diff --git a/tools/video-events-test.js b/tools/video-events-test.js
index c024949..56ea486 100644
--- a/tools/video-events-test.js
+++ b/tools/video-events-test.js
@@ -6,7 +6,7 @@
// version. A note linking such a file, typed video/* in its imeta, plays it
// too. And the clips are in the pager.
// Run: bun tools/video-events-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -90,9 +90,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk]) => {
diff --git a/tools/video-fallback-test.js b/tools/video-fallback-test.js
index 93726e1..ff45846 100644
--- a/tools/video-fallback-test.js
+++ b/tools/video-fallback-test.js
@@ -4,7 +4,7 @@
// post gave no poster wears the server's still. The server (vmedia) is
// stood in for here: its answers are canned.
// Run: bun tools/video-fallback-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -81,9 +81,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-gone-test.js b/tools/video-gone-test.js
index 95826b5..48f2956 100644
--- a/tools/video-gone-test.js
+++ b/tools/video-gone-test.js
@@ -6,7 +6,7 @@
// file's link, its controls and cover gone; the next paint of the post says
// it at once, with no request. A clip on a live host plays as before.
// Run: bun tools/video-gone-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -73,9 +73,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-hls-play-test.js b/tools/video-hls-play-test.js
index f1aea1c..4a18e76 100644
--- a/tools/video-hls-play-test.js
+++ b/tools/video-hls-play-test.js
@@ -4,7 +4,7 @@
// and a post linking an .m3u8 stream plays that. vmedia runs here for real,
// on a local copy of the clip.
// Run: bun tools/video-hls-play-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -90,9 +90,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-tap-test.js b/tools/video-tap-test.js
index 50e5a69..f03aef6 100644
--- a/tools/video-tap-test.js
+++ b/tools/video-tap-test.js
@@ -3,7 +3,7 @@
// rest). With a mouse the middle of the clip is a big play/pause button that
// never goes full screen.
// Run: bun tools/video-tap-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -49,9 +49,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-tile-poster-test.js b/tools/video-tile-poster-test.js
index 48385fe..ed672c2 100644
--- a/tools/video-tile-poster-test.js
+++ b/tools/video-tile-poster-test.js
@@ -4,7 +4,7 @@
// picture for falls back to the frame grab, and one the browser can't load
// either (deleted, its 404 without CORS headers) leaves the grid.
// Run: bun tools/video-tile-poster-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -75,9 +75,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/video-touch-tap-test.js b/tools/video-touch-tap-test.js
index 71ffe9c..ff25095 100644
--- a/tools/video-touch-tap-test.js
+++ b/tools/video-touch-tap-test.js
@@ -5,7 +5,7 @@
// on the control bar or the paused clip's play button stays the player's, a
// drag is a scroll, and where the click does come it isn't taken twice.
// Run: bun tools/video-touch-tap-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -51,9 +51,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
diff --git a/tools/web-comment-test.js b/tools/web-comment-test.js
index 7a6258f..513217b 100644
--- a/tools/web-comment-test.js
+++ b/tools/web-comment-test.js
@@ -9,7 +9,7 @@
// tag beside it, counted as a report of the PERSON (es stayed 0 for an
// 'other' e tag) โ€” two such reports from follows folded the author's every
// post. Run: bun tools/web-comment-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -70,9 +70,9 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pks, ns]) => {
diff --git a/tools/wn-only-device-test.js b/tools/wn-only-device-test.js
index a91262b..7b19876 100644
--- a/tools/wn-only-device-test.js
+++ b/tools/wn-only-device-test.js
@@ -6,7 +6,7 @@
// a fresh one of ours. A device whose key was retired by another sees the
// deletion and stops putting a key back; the card says so there.
// Run: bun tools/wn-only-device-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -76,9 +76,9 @@ try {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
const SK = privateKeyFromSeedWords(mn), ME = getPublicKey(SK);
diff --git a/tools/youtube-tap-test.js b/tools/youtube-tap-test.js
index 271277f..e08058b 100644
--- a/tools/youtube-tap-test.js
+++ b/tools/youtube-tap-test.js
@@ -4,7 +4,7 @@
// over the picture takes them; YouTube's control bar along the bottom is
// left to YouTube, and full screen the layer steps aside.
// Run: bun tools/youtube-tap-test.js
-import puppeteer from './puppeteer.js';
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -60,9 +60,9 @@ async function run(saveData) {
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
- await page.waitForSelector('.words .w .t');
+ await seedWords(page);
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
- await click('skip verification');
+ await skipSeedCheck(page);
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {