diff options
107 files changed, 669 insertions, 444 deletions
@@ -77,7 +77,7 @@ const ui = { confirmClear: false, // "Clear all" confirmation shown editId: null, // account being renamed editLabel: '', - createStep: 'gen', // 'gen' | 'confirm' + createStep: 'intro', // 'intro' (the heads-up) | 'gen' (the words) | 'confirm' (the one-word check) draftMnemonic: '', confirm: [], // [{ index, value }] confirmPass: '', // re-entered passphrase (confirm field, shown once one is typed) @@ -1338,19 +1338,36 @@ function unitTag(cls = '') { // ================================================================ UNLOCK // The create/import card on its own — the unlock screen wraps it in the // classic chrome, the onboarding wizard's "Get started" step shows it bare. +// The front door's seed pages (first run, or "Sign into another account"): +// no tabs, and the page's own Back steps through the create flow. +function seedPages() { + return ui.screen === 'unlock' && !!((ui.onb && ui.onb.step === 'seed') || (ui.fromWallet && ui.addFlow)); +} +// Back within the create flow: the check → the passphrase → the words → the +// heads-up. False when there's no earlier step (the caller leaves the flow). +const CREATE_PREV = { gen: 'intro', pass: 'gen', confirm: 'pass' }; +function createStepBack() { + if (ui.unlockTab !== 'create' || ui.entropyPage || !CREATE_PREV[ui.createStep]) return false; + ui.createStep = CREATE_PREV[ui.createStep]; + ui.unlockError = ''; + render(); + return true; +} + function unlockCard() { // The entropy page takes the card over entirely — seed words, tabs and // other controls step aside until Submit or Back. if (ui.entropyPage) return h('div', { class: 'card col' }, entropyPage()); // Inside the first-run wizard the words are already on screen: no tabs, // just a line underneath for whoever brought their own seed (and one back). - const wizard = ui.screen === 'unlock' && ui.onb && ui.onb.step === 'seed'; - const swap = wizard ? h('button', { + const wizard = seedPages(); + // only before the words are up: once they're shown, it was already offered + const swap = wizard && (ui.unlockTab === 'import' || ui.createStep === 'intro') ? h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { clearSeedDrafts(); ui.unlockError = ''; if (ui.unlockTab === 'create') ui.unlockTab = 'import'; - else { ui.unlockTab = 'create'; ui.createStep = 'gen'; ui.draftRandom = ui.draftMnemonic = newMnemonic(); } + else { ui.unlockTab = 'create'; ui.createStep = 'intro'; ui.draftRandom = ui.draftMnemonic = newMnemonic(); } render(); }, }, ui.unlockTab === 'create' ? t('onbHaveSeed') : t('onbCreateInstead')) : null; @@ -1369,7 +1386,77 @@ function unlockCard() { ); } +// "Sign in to another account", laid out as the front door is (Adam, +// 2026-11-07): two choices, then the sign-in list with the seed phrase +// first. Everything here adds an account beside the open ones; Back returns +// to the wallet. +function addAccountScreen() { + const page = (kids) => h('div', { class: 'col onb', style: 'gap:20px' }, ...kids); + const title = (txt) => h('h2', { class: 'onb-title' }, txt); + const toWallet = () => { + ui.fromWallet = false; ui.addFlow = false; ui.addStep = null; ui.nostrLoginOpen = false; + ui.screen = 'wallet'; ui.unlockError = ''; ui.nostrLoginError = ''; render(); + }; + if (ui.addStep === 'doors') { + if (ui.nostrLoginOpen) { + return page([ + h('div', { class: 'onb-mark', html: NOSTR_MARK }), + title(t('onbSigninTitle')), + featureHook('unlockExtra') || h('div', { class: 'notice err' }, 'nostr login unavailable'), + h('button', { class: 'linklike small', onClick: () => { ui.nostrLoginOpen = false; ui.nostrLoginError = ''; render(); } }, t('back')), + ]); + } + return page([ + h('div', { class: 'onb-hero' }, brandHeader(false)), + title(t('onbSigninTitle')), + h('div', { class: 'col', style: 'gap:10px' }, + h('button', { id: 'add-restore', class: 'btn-block', style: 'padding:14px', onClick: () => { + clearSeedDrafts(); + ui.unlockTab = 'import'; ui.unlockError = ''; ui.addStep = null; + render(); + } }, t('onbRestoreDoor')), + featureHook('frontDoorSignin', { short: true })), + h('button', { class: 'linklike small', onClick: () => { ui.nostrLoginError = ''; ui.addStep = 'welcome'; render(); } }, t('back')), + ]); + } + return page([ + h('div', { class: 'onb-hero' }, brandHeader(false)), + title(t('addAccountTitle')), + h('div', { class: 'col', style: 'gap:10px' }, + h('button', { + id: 'add-create', + class: 'btn-primary btn-block', style: 'font-size:17px;padding:14px', onClick: () => { + clearSeedDrafts(); + ui.unlockTab = 'create'; ui.unlockError = ''; ui.createStep = 'intro'; ui.confirm = []; + ui.draftRandom = ui.draftMnemonic = newMnemonic(); + ui.addStep = null; + render(); + }, + }, t('onbStart')), + h('button', { + id: 'add-signin', + class: 'btn-block', style: 'font-size:17px;padding:14px', + onClick: () => { ui.nostrLoginError = ''; ui.addStep = 'doors'; render(); }, + }, t('onbSignInExisting'))), + accounts.length ? h('button', { class: 'linklike small', onClick: toWallet }, t('back')) : null, + ]); +} + function unlockScreen() { + if (ui.fromWallet && ui.addFlow) { + if (ui.addStep && !ui.entropyPage) return addAccountScreen(); + // a choice made there: the same seed pages as the front door, whose + // Back steps back to where it was made + return h('div', { class: 'col onb', style: 'gap:20px' }, + h('h2', { class: 'onb-title' }, t(ui.unlockTab === 'import' ? 'onbRestoreTitle' : 'onbSeedTitle')), + unlockCard(), + ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { + if (createStepBack()) return; + const from = ui.unlockTab === 'import' ? 'doors' : 'welcome'; + clearSeedDrafts(); ui.draftMnemonic = ''; ui.createStep = 'intro'; ui.confirm = []; ui.unlockError = ''; + ui.addStep = from; render(); + } }, t('back'))); + } return h( 'div', { class: 'col', style: 'gap:16px' }, @@ -1476,6 +1563,27 @@ function tabBtn(label, active, onClick) { } function createPane() { + // A heads-up before the words appear: they are secret, and the screen is + // about to show them (Adam, 2026-11-07). No warning icon — a plain note. + if (ui.createStep === 'intro' || !ui.createStep) { + const n = ui.draftMnemonic ? ui.draftMnemonic.split(' ').length : 12; + return h( + 'div', + { class: 'col seed-intro' }, + h('p', { style: 'margin:0' }, t('seedIntroBody', { n })), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { + id: 'seed-intro-next', + class: 'btn-primary btn-block', style: 'padding:14px', + onClick: () => { + if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic(); + ui.unlockError = ''; + ui.createStep = 'gen'; + render(); + }, + }, t('seedIntroShow')), + ); + } if (ui.createStep === 'gen') { // The wizard shows words, never a Generate button: a refresh restores // the step but not the draft (drafts live in memory only), so draw a @@ -1513,74 +1621,78 @@ function createPane() { h('button', { class: 'btn-block', style: 'display:flex;align-items:center;justify-content:center;gap:8px', onClick: () => copy(ui.draftMnemonic) }, h('span', { style: 'display:flex', html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>' }), t('copy')), - optionsPanel(), h( 'button', { + id: 'seed-next', class: 'btn-primary btn-block', - onClick: () => { - // catch a mistyped passphrase here, while the field to fix it is - // still on screen — the verify step only samples the words - if (ui.passphrase && ui.confirmPass !== ui.passphrase) { - ui.unlockError = t('passphraseMismatch'); render(); return; - } - ui.confirm = pickConfirm(words); - ui.unlockError = ''; - ui.createStep = 'confirm'; - render(); - }, + onClick: () => { ui.unlockError = ''; ui.createStep = 'pass'; render(); }, }, - t('verifyBackup') - ), - h( - 'button', - { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic, { generated: true }) }, - t('skipVerification') + t('next') ) ); } - // confirm step (optional — reachable via "Verify backup") + // The passphrase, on a page of its own (Adam, 2026-11-07): what it is, + // that it's optional but recommended, and the field — a small "Add + // passphrase" button under the words was easy to miss. + if (ui.createStep === 'pass') { + const words = ui.draftMnemonic.split(' '); + const next = () => { + // a typo here IS a different wallet: caught while the fields are up + if (ui.passphrase && ui.confirmPass !== ui.passphrase) { + ui.unlockError = t('passphraseMismatch'); render(); return; + } + ui.confirm = pickConfirm(words); + ui.unlockError = ''; + ui.createStep = 'confirm'; + render(); + }; + return h( + 'div', + { class: 'col' }, + h('h3', { style: 'margin:0' }, t('passStepTitle')), + h('p', { style: 'margin:0' }, t('passStepBody', { n: words.length })), + h('p', { class: 'muted', style: 'margin:0' }, t('passStepNote')), + ...passFields(), + h('button', { id: 'pass-next', class: 'btn-primary btn-block', onClick: next }, t(ui.passphrase ? 'next' : 'passStepNone')), + ); + } + // The check: which word is #5? One tap from a few of the phrase's own + // words, or Skip (Adam, 2026-11-07) — no typing. + const q = ui.confirm; + if (!q || !q.options) { ui.createStep = 'pass'; return createPane(); } return h( 'div', { class: 'col' }, - h('p', { class: 'muted' }, t('confirmBackupIntro')), - ...ui.confirm.map((c, i) => - h( - 'label', - { class: 'field' }, - h('span', { class: 'lab' }, t('wordN', { n: c.index + 1 })), - h('input', { - type: 'text', - class: 'mono-input', - autocapitalize: 'none', - autocomplete: 'off', - spellcheck: 'false', - value: c.value, - onInput: (e) => (ui.confirm[i].value = e.target.value.trim()), - }) - ) - ), - h('div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.createStep = 'gen'; render(); } }, t('back')), + h('p', { style: 'margin:0' }, t('quizPrompt', { n: q.index + 1 })), + h('div', { class: 'quiz-opts' }, q.options.map((w) => h('button', { - class: 'btn-primary grow', + type: 'button', + class: 'quiz-opt' + (q.wrong && q.wrong.includes(w) ? ' wrong' : ''), onClick: () => { const words = ui.draftMnemonic.split(' '); - const ok = ui.confirm.every((c) => c.value.toLowerCase() === words[c.index]); - if (!ok) { ui.unlockError = t('wordsMismatch'); render(); return; } - openWallet(ui.draftMnemonic, { generated: true }); + if (w === words[q.index]) { ui.unlockError = ''; openWallet(ui.draftMnemonic, { generated: true }); return; } + (q.wrong ||= []).includes(w) || q.wrong.push(w); + render(); }, - }, t('openWallet')) - ), - h('button', { class: 'btn-block', onClick: () => openWallet(ui.draftMnemonic, { generated: true }) }, t('skipVerification')) + }, w))), + q.wrong && q.wrong.length ? h('div', { class: 'notice err' }, t('quizWrong')) : null, + h('button', { id: 'seed-skip', class: 'btn-ghost btn-block', onClick: () => { ui.unlockError = ''; openWallet(ui.draftMnemonic, { generated: true }); } }, t('skip')), + // outside the seed pages (no Back of their own), a way back to the words + seedPages() ? null : h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { ui.createStep = 'pass'; ui.unlockError = ''; render(); } }, t('back')), ); } -function pickConfirm(words) { - const idx = new Set(); - while (idx.size < 3) idx.add(Math.floor(Math.random() * words.length)); - return [...idx].sort((a, b) => a - b).map((index) => ({ index, value: '' })); +// The 5th word and three others from the same phrase, shuffled: knowing the +// words isn't enough, you need them in order. +function pickConfirm(words, index = Math.min(4, words.length - 1)) { + const right = words[index]; + const others = [...new Set(words.filter((w) => w !== right))]; + for (let i = others.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [others[i], others[j]] = [others[j], others[i]]; } + const options = [right, ...others.slice(0, 3)]; + for (let i = options.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [options[i], options[j]] = [options[j], options[i]]; } + return { index, options, wrong: [] }; } // The BYO-entropy page, shared by Create (re-derives the draft phrase) and @@ -1674,22 +1786,28 @@ function optionsPanel() { // already typed keeps it open. if (!ui.passOpen && !ui.passphrase) { return [h('button', { - class: 'btn-ghost btn-sm', type: 'button', style: 'align-self:flex-start', + class: 'btn-block', type: 'button', onClick: (e) => { e.preventDefault(); ui.passOpen = true; render(); }, }, t('addPassphrase'))]; } + return passFields({ label: true }); +} + +// The passphrase field (+ its confirmation once something is typed). The +// create flow's own page explains it above; the import pane labels it here. +function passFields({ label = false } = {}) { const pass = h( 'label', { class: 'field' }, h('span', { class: 'lab' }, - t('passphraseOptional'), + t(label ? 'passphraseOptional' : 'passphrase'), // People assume a password logs in or encrypts; a BIP39 passphrase does // neither, and getting that wrong loses money — worth a sentence here. h('button', { class: 'linklike small', type: 'button', style: 'margin-left:8px', onClick: (e) => { e.preventDefault(); ui.passInfoOpen = !ui.passInfoOpen; render(); }, }, t('passWhatsThis'))), - ui.passInfoOpen ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passInfo')) : h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passOptionalHint')), + ui.passInfoOpen ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passInfo')) : label ? h('p', { class: 'small muted', style: 'margin:0 0 6px' }, t('passOptionalHint')) : null, h( 'div', { class: 'input-group' }, @@ -1718,7 +1836,7 @@ function optionsPanel() { class: 'mono-input', autocomplete: 'off', value: ui.confirmPass, - onInput: (e) => (ui.confirmPass = e.target.value), + onInput: (e) => { ui.confirmPass = e.target.value; if (ui.unlockError && ui.confirmPass === ui.passphrase) { ui.unlockError = ''; render(); } }, }) ) : null; @@ -1929,8 +2047,9 @@ async function activateAccount(acc, opts = {}) { function signInAnother() { clearSeedDrafts(); ui.fromWallet = true; ui.unlockError = ''; ui.screen = 'unlock'; - // Starts on Create new (its Generate button), not the seed-import box. - ui.unlockTab = 'create'; ui.createStep = 'gen'; ui.draftMnemonic = ''; ui.confirm = []; + ui.addFlow = true; ui.addStep = 'welcome'; ui.nostrLoginOpen = false; ui.nostrLoginError = ''; + // Opens on the front door's two choices (addAccountScreen). + ui.unlockTab = 'create'; ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.confirm = []; ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; render(); } @@ -2803,7 +2922,7 @@ function lock({ offerPassword = false } = {}) { ui.confirmRemove = null; ui.editId = null; ui.editLabel = ''; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.importText = ''; ui.entropyPage = null; @@ -3385,7 +3504,7 @@ function goHome() { ui.sendError = ''; ui.fromWallet = false; } else if ((ui.onb && ui.onb.step !== 'welcome') - || ui.draftMnemonic || ui.createStep !== 'gen' || ui.importText) { + || ui.draftMnemonic || !['intro', 'gen'].includes(ui.createStep) || ui.importText) { // Mid-create with no wallet minted yet (wizard step, a generated seed on // screen, the confirm quiz, a pasted import): the logo backs out to the // start page this began from — not the vault prompt, which reads as a @@ -3395,7 +3514,7 @@ function goHome() { ui.nostrLoginOpen = false; ui.screen = 'unlock'; ui.unlockTab = 'create'; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; clearSeedDrafts(); ui.entropyPage = null; @@ -3414,7 +3533,7 @@ function goHome() { } else { ui.screen = 'unlock'; ui.unlockTab = 'create'; - ui.createStep = 'gen'; + ui.createStep = 'intro'; ui.draftMnemonic = ''; ui.confirm = []; ui.unlockError = ''; @@ -3494,8 +3613,8 @@ function accountsScreen() { if (aw.from === 'new') { ui.newWalletKind = kind; ui.addWallet = null; - ui.draftMnemonic = ''; ui.createStep = 'gen'; ui.confirm = []; ui.passphrase = ''; ui.confirmPass = ''; ui.passOpen = false; - ui.showPass = false; ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = true; ui.unlockError = ''; + ui.draftMnemonic = ''; ui.createStep = 'intro'; ui.confirm = []; ui.passphrase = ''; ui.confirmPass = ''; ui.passOpen = false; + ui.showPass = false; ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = true; ui.unlockError = ''; ui.addFlow = false; render(); } else { createDerivedWallet(aw.from, kind); @@ -4190,7 +4309,7 @@ function onboardScreen() { const startCreate = () => { ui.unlockTab = 'create'; ui.unlockError = ''; - ui.createStep = 'gen'; + ui.createStep = 'intro'; if (!ui.draftMnemonic) ui.draftRandom = ui.draftMnemonic = newMnemonic(); o.step = 'seed'; render(); @@ -4292,26 +4411,36 @@ function onboardScreen() { ui.tab = 'receive'; render(); }; + // Later means later, not never: seedSeen stays false, so the wallet + // keeps a quiet reminder until the phrase has actually been seen. + // Except on the way to a coinos.io move: the balance about to arrive + // must have its twelve words written down first, so Later isn't offered. + const later = migrateLinkPending() ? null : h('button', { class: 'btn-ghost btn-block', onClick: () => { + if (canOfferSpending()) { o.step = 'spend'; o.enterAddr = undefined; render(); return; } + ui.onb = null; + try { localStorage.removeItem(ONB_STEP_KEY); } catch {} + ui.account = 'savings'; + try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {} + ui.tab = 'receive'; + render(); + } }, t('onbBackupLater')); + // a heads-up first: the next screen shows the secret (Adam, 2026-11-07) + if (!o.backupShown) { + return page([ + title(t('onbBackupTitle')), + h('p', { class: 'muted', style: 'margin:0' }, t('onbBackupBody')), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { id: 'onb-backup-show', class: 'btn-primary btn-block', style: 'padding:14px', onClick: () => { o.backupShown = true; render(); } }, t('onbBackupShow')), + later, + ]); + } return page([ title(t('onbBackupTitle')), - h('p', { class: 'muted', style: 'margin:0' }, t('onbBackupBody')), h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w)))), h('div', { class: 'row gap6' }, copyBtn(acc.mnemonic, t('copyPhrase'))), h('button', { class: 'btn-primary btn-block', style: 'padding:14px', onClick: done }, t('onbBackupDone')), - // Later means later, not never: seedSeen stays false, so the wallet - // keeps a quiet reminder until the phrase has actually been seen. - // Except on the way to a coinos.io move: the balance about to arrive - // must have its twelve words written down first, so Later isn't offered. - migrateLinkPending() ? null : h('button', { class: 'btn-ghost btn-block', onClick: () => { - if (canOfferSpending()) { o.step = 'spend'; o.enterAddr = undefined; render(); return; } - ui.onb = null; - try { localStorage.removeItem(ONB_STEP_KEY); } catch {} - ui.account = 'savings'; - try { localStorage.setItem(ACCOUNT_KEY, 'savings'); } catch {} - ui.tab = 'receive'; - render(); - } }, t('onbBackupLater')), + later, ]); } if (o.step === 'seed') { @@ -4322,7 +4451,7 @@ function onboardScreen() { title(t(ui.unlockTab === 'import' ? 'onbRestoreTitle' : 'onbSeedTitle')), unlockCard(), // the entropy page brings its own Back — one is enough - ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { ui.unlockError = ''; o.step = ui.unlockTab === 'import' ? 'doors' : 'welcome'; render(); } }, t('back')), + ui.entropyPage ? null : h('button', { class: 'linklike small', onClick: () => { if (createStepBack()) return; ui.unlockError = ''; o.step = ui.unlockTab === 'import' ? 'doors' : 'welcome'; render(); } }, t('back')), ]); } if (o.step === 'signin') { diff --git a/src/features/gifts.js b/src/features/gifts.js index 42e794d..f23ecc8 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -288,51 +288,47 @@ export function giftsFeature(ctx) { h('h2', { style: 'margin:0' }, claimed ? t('claimedTitle') : t('newWalletTitle')), h('p', { class: 'muted', style: 'margin:0' }, claimed ? t('claimedBody') : t('newWalletBody')) ), - h('div', { class: 'card col' }, - h('h3', {}, t('recoveryPhrase')), - h('div', { class: 'warn-box' }, t('writeDownWarn')), - h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, - words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w))) - ), - h('div', { class: 'row gap6' }, copyBtn(wallet.mnemonic, t('copyPhrase'))), - h('button', { class: 'btn-primary btn-block', onClick: () => { ui.confirm = pickConfirm(words); ui.claimError = ''; ui.claimStep = 'verify'; render(); } }, t('verifyBackup')), - h('button', { class: 'btn-block', onClick: finishClaim }, t('skipVerification')) - ) + // a heads-up before the words, then the words, then one word to + // check (Adam, 2026-11-07) — the same three steps as a new wallet + !ui.claimShown + ? h('div', { class: 'card col' }, + h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), + h('p', { style: 'margin:0' }, t('seedIntroBody', { n: words.length })), + h('p', { class: 'muted', style: 'margin:0' }, t('seedIntroPrivate')), + h('button', { id: 'claim-seed-show', class: 'btn-primary btn-block', onClick: () => { ui.claimShown = true; render(); } }, t('seedIntroShow'))) + : h('div', { class: 'card col' }, + h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), + h('div', { class: 'words', style: '--rows-3:' + Math.ceil(words.length / 3) + ';--rows-2:' + Math.ceil(words.length / 2) }, + words.map((w, i) => h('div', { class: 'w' }, h('span', { class: 'n' }, i + 1), h('span', { class: 't' }, w))) + ), + h('div', { class: 'row gap6' }, copyBtn(wallet.mnemonic, t('copyPhrase'))), + h('button', { id: 'claim-seed-next', class: 'btn-primary btn-block', onClick: () => { ui.confirm = pickConfirm(words); ui.claimError = ''; ui.claimStep = 'verify'; render(); } }, t('next'))) ); } if (ui.claimStep === 'verify') { - // Same word-confirmation as new-wallet creation: prove the recipient wrote - // the phrase down before sending them off into their freshly funded wallet. const words = wallet.mnemonic.split(' '); + const q = ui.confirm; + if (!q || !q.options) { ui.claimStep = 'backup'; ui.claimShown = true; return claimScreen(); } return h( 'div', { class: 'col', style: 'gap:16px' }, brandHeader(false), h('div', { class: 'card col' }, h('h3', { style: 'margin-top:0' }, t('recoveryPhrase')), - h('p', { class: 'muted', style: 'margin:0' }, t('confirmBackupIntro')), - ...ui.confirm.map((c, i) => - h('label', { class: 'field' }, - h('span', { class: 'lab' }, t('wordN', { n: c.index + 1 })), - h('input', { - type: 'text', class: 'mono-input', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', - value: c.value, onInput: (e) => (ui.confirm[i].value = e.target.value.trim()), - }) - ) - ), - ui.claimError && h('div', { class: 'notice err' }, ui.claimError), - h('div', { class: 'row gap6' }, - h('button', { class: 'btn-ghost', onClick: () => { ui.claimStep = 'backup'; ui.claimError = ''; render(); } }, t('back')), + h('p', { style: 'margin:0' }, t('quizPrompt', { n: q.index + 1 })), + h('div', { class: 'quiz-opts' }, q.options.map((w) => h('button', { - class: 'btn-primary grow', + type: 'button', + class: 'quiz-opt' + (q.wrong.includes(w) ? ' wrong' : ''), onClick: () => { - const ok = ui.confirm.every((c) => c.value.toLowerCase() === words[c.index]); - if (!ok) { ui.claimError = t('wordsMismatch'); render(); return; } - finishClaim(); + if (w === words[q.index]) { ui.claimError = ''; finishClaim(); return; } + if (!q.wrong.includes(w)) q.wrong.push(w); + ui.claimError = t('quizWrong'); render(); }, - }, t('openWallet')) - ), - h('button', { class: 'btn-block', onClick: finishClaim }, t('skipVerification')) + }, w))), + ui.claimError && h('div', { class: 'notice err' }, ui.claimError), + h('button', { id: 'claim-seed-skip', class: 'btn-ghost btn-block', onClick: () => { ui.claimError = ''; finishClaim(); } }, t('skip')), + h('button', { class: 'linklike small', style: 'align-self:center', onClick: () => { ui.claimStep = 'backup'; ui.claimShown = true; ui.claimError = ''; render(); } }, t('quizBack')), ) ); } @@ -366,7 +362,7 @@ export function giftsFeature(ctx) { // view the claim on a block explorer, or head back to the home screen. h('button', { class: 'btn-primary btn-block', onClick: () => { ui.claimTaken = null; ui.claimedAmount = 0; - if (activeAccount() && activeAccount().provisional) { ui.claimStep = 'backup'; commitAccount(); } + if (activeAccount() && activeAccount().provisional) { ui.claimStep = 'backup'; ui.claimShown = false; commitAccount(); } else { ui.screen = 'wallet'; ui.claimStep = null; landAccount(); } render(); } }, activeAccount() && activeAccount().provisional ? t('createWalletAnyway') : t('goToWallet')), @@ -488,7 +484,7 @@ export function giftsFeature(ctx) { commitAccount(); // claimed — keep a fresh gift wallet (no-op for an existing one) if (ui.claimFrom) hook('followGiftSender', ui.claimFrom); if (wasProvisional) { - ui.claimStep = 'backup'; // fresh wallet — show the new seed to back up + ui.claimStep = 'backup'; ui.claimShown = false; // fresh wallet — show the new seed to back up } else { ui.screen = 'wallet'; // existing wallet — straight into it, no new seed to back up ui.claimStep = null; 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/i18n.js b/src/i18n.js index e45fa49..b45638e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -197,6 +197,19 @@ const STR = { skipVerification: 'Skip verification', confirmBackupIntro: 'Confirm your backup by entering the requested words.', wordN: 'Word #{n}', + seedIntroBody: 'Next you’ll see your seed phrase: {n} words that are your wallet. Write them down on paper, in order, and keep them somewhere safe. They’re the only way to get your coins back if you lose this device.', + seedIntroPrivate: 'Anyone who sees them can take your coins, so make sure no one is looking at your screen.', + seedIntroShow: 'Show seed phrase', + next: 'Next', + passStepTitle: 'Add a passphrase?', + passStepBody: 'A passphrase is an extra secret you choose yourself, on top of your {n} words. Anyone who finds your seed phrase still can’t open your wallet without it.', + passStepNote: 'It’s optional, but recommended. Write it down with your seed phrase: you’ll need both to recover your wallet, exactly as written, and nobody can reset it for you.', + passStepNone: 'Continue without one', + skip: 'Skip', + quizPrompt: 'Which one is word #{n}?', + quizWrong: 'Not that one. Check what you wrote down.', + quizBack: 'Show the words again', + onbBackupShow: 'Show seed phrase', reenterPassphrase: 'Re-enter passphrase', back: '← Back', cancel: 'Cancel', @@ -1409,6 +1422,7 @@ const STR = { doorPasskey: 'Passkey', doorGoogle: 'Google', doorNostr: 'Nostr', + addAccountTitle: 'Add an account', onbSignInExisting: 'Sign in to an existing account', onbSigninTitle: 'Sign in', onbLegacyTitle: 'Move your coinos.io account', diff --git a/src/style.css b/src/style.css index dfe9b9d..d753604 100644 --- a/src/style.css +++ b/src/style.css @@ -344,6 +344,10 @@ textarea.send-dest::placeholder { font-family: var(--sans); } eight-letter word ("resemble") pushed the third column out past the card's padding. The cells are a touch tighter so every BIP-39 word (eight letters at most) still fits whole in a third of a narrow card. */ +/* The one-word check after the phrase: four of its words, tap the right one. */ +.quiz-opts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } +.quiz-opt { font-family: var(--mono); font-size: 16px; padding: 14px 8px; } +.quiz-opt.wrong { border-color: var(--red); color: var(--red); background: var(--red-soft); } .words { display: grid; grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .words .w { display: flex; align-items: baseline; gap: 5px; background: var(--surface2); min-width: 0; diff --git a/static/coinos-beta.apk b/static/coinos-beta.apk Binary files differindex 4da2398..4054941 100644 --- a/static/coinos-beta.apk +++ b/static/coinos-beta.apk diff --git a/tools/account-access-test.js b/tools/account-access-test.js index 3e481cc..91fbe05 100644 --- a/tools/account-access-test.js +++ b/tools/account-access-test.js @@ -36,13 +36,18 @@ try { check('avatar opens the wallet settings with sign-in and logout', await page.evaluate(() => ['Security', 'Sign into another account', 'Log out'].every((s) => document.body.innerText.includes(s)))); const saved = await page.evaluate(() => localStorage.getItem('btc-wallet-watch')); await click('Sign into another account'); - check('sign-in opens on Create new, not the seed import box', await page.evaluate(() => - document.querySelector('.tabs .active')?.textContent.includes('Create new') && !document.querySelector('.card textarea'))); + check('sign-in opens on the front door\'s two choices', await page.evaluate(() => + !!document.getElementById('add-create') && !!document.getElementById('add-signin') && !document.querySelector('.tabs') && !document.querySelector('.card textarea'))); + await click('Sign in to an existing account'); + check('...then the sign-in list, seed phrase first', await page.evaluate(() => { + const b = [...document.querySelectorAll('.onb button')].map((x) => x.textContent.trim()); + return b[0] === 'Seed phrase' && b.includes('Nostr'); + })); // Expand the Nostr login card without supplying a private key. - await click('Sign in with Nostr'); + await click('Nostr'); await page.waitForSelector('input[placeholder="nsec, private key, or bunker://…"]'); check('Nostr key sign-in is reachable without deleting the old wallet', await page.evaluate((saved) => localStorage.getItem('btc-wallet-watch') === saved, saved)); - await click('← Back'); + for (let i = 0; i < 3; i++) { await click('← Back'); await new Promise((r) => setTimeout(r, 150)); } await page.waitForSelector('.header-avatar'); await page.click('.header-avatar'); await click('Log out'); 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/gift-profile-setup-test.js b/tools/gift-profile-setup-test.js index 9304d1a..04e58c5 100644 --- a/tools/gift-profile-setup-test.js +++ b/tools/gift-profile-setup-test.js @@ -53,7 +53,11 @@ try { page.on('pageerror', (e) => errors.push(String(e))); await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 }); await toSeed(page, 'Happy birthday! 🎂 <b>not bold</b>'); - await click(page, 'skip verification'); + check('a heads-up comes before the words', /no one is looking/i.test(await text(page)) && await page.evaluate(() => !document.querySelector('.words'))); + await click(page, 'show seed phrase'); + check('...then the words, with Next', await page.evaluate(() => document.querySelectorAll('.words .w').length === 12) && await click(page, 'next')); + check('...then one word to pick, or Skip', await waitText(page, /which one is word #5/i, 5000) && await page.evaluate(() => document.querySelectorAll('.quiz-opt').length === 4)); + await click(page, 'skip'); check('the seed done: the wizard asks for a username', await waitText(page, /now pick a username/i, 10000), (await text(page)).slice(0, 200)); check('...with the claim form (or its wait) and Not now', await page.evaluate(() => !!document.querySelector('.onb')) && /not now/i.test(await text(page))); if (process.argv[2]) await page.screenshot({ path: process.argv[2] }); @@ -66,30 +70,25 @@ try { p2.on('pageerror', (e) => errors.push(String(e))); await p2.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 }); await toSeed(p2); - check('Verify backup opens the word check', await click(p2, 'verify') && await waitText(p2, /word #?\d/i, 5000)); - await sleep(500); - // the verify page doesn't show the words: read them from the account - const filled = await p2.evaluate(() => { - const accs = JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}'); - const list = Array.isArray(accs) ? accs : accs.accounts || accs.list || []; - const m = list.map((a) => a.mnemonic).filter(Boolean).pop(); if (!m) return false; - const w = m.split(' '); - for (const lab of document.querySelectorAll('label.field')) { - const n = Number(lab.querySelector('.lab').textContent.match(/\d+/)[0]); - const inp = lab.querySelector('input'); - const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; - set.call(inp, w[n - 1]); inp.dispatchEvent(new Event('input', { bubbles: true })); - } - return true; - }); + await click(p2, 'show seed phrase'); + const fifth = await p2.evaluate(() => document.querySelectorAll('.words .w .t')[4]?.textContent.trim()); + check('Next opens the word check', await click(p2, 'next') && await waitText(p2, /which one is word #5/i, 5000)); + // a wrong pick says so and stays + const wrongOk = await p2.evaluate((right) => { + const b = [...document.querySelectorAll('.quiz-opt')].find((x) => x.textContent.trim() !== right); b?.click(); return !!b; + }, fifth); + await sleep(300); + check('...a wrong word is marked, and the check stays', wrongOk && await p2.evaluate(() => !!document.querySelector('.quiz-opt.wrong')) && /not that one/i.test(await text(p2))); + const filled = await p2.evaluate((right) => { + const b = [...document.querySelectorAll('.quiz-opt')].find((x) => x.textContent.trim() === right); b?.click(); return !!b; + }, fifth); if (filled) { - await click(p2, 'open wallet'); check('a verified seed leads to the username step too', await waitText(p2, /now pick a username/i, 10000)); await click(p2, 'not now'); let gone = false; for (let i = 0; i < 20 && !gone; i++) { await sleep(250); gone = !(await p2.evaluate(() => !!document.querySelector('.onb'))); } check('Not now: into the wallet, the wizard gone', gone, (await text(p2)).slice(0, 200)); - } else console.log(' (accounts not in localStorage — verify path not driven)'); + } else check('the right word is among the choices', false); check('no page errors', errors.length === 0, errors.join(' | ')); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ a gift\'s new wallet is offered a name, a picture and a cover' : '\n❌ failed'); 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/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-backup-test.js b/tools/onboard-backup-test.js index 3eefb02..8fc1071 100644 --- a/tools/onboard-backup-test.js +++ b/tools/onboard-backup-test.js @@ -87,8 +87,11 @@ try { // words on screen at once, with what they are. await page.evaluate(() => document.getElementById('onb-create')?.click()); await sleep(600); - // the warning box that said "write these 12 words down" is gone - // (2026-11-06): the words themselves are the explanation + // a heads-up first, no warning icon (2026-11-07): sensitive words next + const intro = await page.evaluate(() => ({ words: document.querySelectorAll('.words .w').length, text: document.body.innerText, warn: !!document.querySelector('.warn-box') })); + check('the words wait behind a heads-up to write them down', intro.words === 0 && /write them down/i.test(intro.text) && !intro.warn && !/⚠/.test(intro.text)); + await page.evaluate(() => document.getElementById('seed-intro-next')?.click()); + await sleep(400); const made = await page.evaluate(() => document.querySelectorAll('.words .w .t').length); check('the way in shows what the wallet actually is: its twelve words', made === 12, made + ' words'); await page.close(); @@ -135,6 +138,9 @@ try { { const { page, ctx, seed } = await signIn(); check('a sign-in is asked to write the phrase down', await waitText(page, 'write down your recovery phrase')); + check('...with the words behind Show seed phrase', await page.evaluate(() => !document.querySelector('.words'))); + await click(page, 'show seed phrase'); + await sleep(300); const shown = await page.evaluate(() => [...document.querySelectorAll('.words .w .t')].map((e) => e.textContent.trim())); check('...and the real twelve words are on screen', shown.length === 12 && shown.join(' ') === seed, shown.length + ' words' + (shown.join(' ') === seed ? ', matching the derived seed' : ', WRONG')); 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/padlock-flow-test.js b/tools/padlock-flow-test.js index dcc0a85..f56d557 100644 --- a/tools/padlock-flow-test.js +++ b/tools/padlock-flow-test.js @@ -110,7 +110,9 @@ try { await p2.evaluate(() => document.querySelector('.header-avatar')?.click()); await sleep(800); await click2('Sign into another account'); await sleep(500); await click2('Sign into a new account'); await sleep(500); - await click2('Import existing'); await sleep(300); // the sign-in screen's import tab + // the front door's layout (2026-11-07): sign in → Seed phrase + await click2('Sign in to an existing account'); await sleep(300); + await click2('Seed phrase'); await sleep(300); return importWallet(); }; // the wallets on this device, as the identity switcher lists them (the 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]) => { |
