diff options
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]) => { |
