From c8e6de17311e3eaa18a73c2aa0688c80e89db254 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 22:46:19 -0700 Subject: Get an address immediately, without waiting on a signer prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The passive setup path asked the browser extension to sign the claim, so it sat behind a permission popup — 20s per attempt, retried, hence the minute-long spinner ending in the manual form. The wallet's own key now makes that claim: no signer, no prompt, no wait. Tying the address to the user's real npub happens at login instead, where the signer is already live, and nominates the wallet key to manage the record after. Failures are visible now rather than hidden: the pane shows why setup failed, the watchdog is 12s instead of 60s, internal timeouts are tighter, and the registrar logs every refusal with its reason. Co-Authored-By: Claude Fable 5 --- src/features/names.js | 53 ++++++++++++++++++++++++---------------------- src/features/nostrlogin.js | 4 ++++ src/i18n.js | 1 + 3 files changed, 33 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/features/names.js b/src/features/names.js index 48fb72a..ec422be 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -41,10 +41,10 @@ export function namesFeature(ctx) { async function post(path, method, payload, signer) { const url = `${REGISTRAR}${path}`; const body = JSON.stringify(payload); - const auth = await withTimeout(nip98Header(signer || walletSigner(), url, method, body), 20000, 'signing'); + const auth = await withTimeout(nip98Header(signer || walletSigner(), url, method, body), 12000, 'signing'); const r = await withTimeout(fetch(url, { method, headers: { 'content-type': 'application/json', authorization: auth }, body, - }), 20000, 'registrar'); + }), 12000, 'registrar'); const j = await r.json().catch(() => ({})); if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`); return j; @@ -57,7 +57,7 @@ export function namesFeature(ctx) { && !!wallet.nostrSign && !!hook('arkReady'); async function currentUri() { - const addr = await withTimeout(hook('arkStaticAddress'), 20000, 'ark'); + const addr = await withTimeout(hook('arkStaticAddress'), 12000, 'ark'); return addr ? `bitcoin:?ark=${encodeURIComponent(addr)}` : null; } @@ -91,12 +91,13 @@ export function namesFeature(ctx) { // out), so nothing here ever blocks the wallet. let retryTimer = null; let deadline = null; + let lastError = null; async function refresh(attempt = 0) { clearTimeout(retryTimer); // Whatever goes wrong, stop claiming to be "setting up" after a minute // and show the user the manual form instead. if (!deadline) { - deadline = setTimeout(() => { checked = true; clearTimeout(retryTimer); render(); }, 60000); + deadline = setTimeout(() => { checked = true; clearTimeout(retryTimer); render(); }, 12000); } try { if (!available()) { @@ -117,25 +118,12 @@ export function namesFeature(ctx) { // usually be re-attached silently; anything else falls back to the // wallet's own identity rather than retrying a claim that can only // ever fail. - const linked = hook('nostrLoginIdentity'); - let signer = linked && linked.signer; - if (linked && !signer) { try { signer = await withTimeout(hook('nostrLoginResume'), 10000, 'signer'); } catch {} } - const useReal = !!(linked && signer); - const npub = useReal ? linked.npub : (wallet.nostrNpub && wallet.nostrNpub()); - if (npub) { - const opts = useReal ? { signer, manager: wallet.nostrPubkey() } : {}; - try { - await claim(npub.slice(0, 20), opts); - } catch (e) { - // A refusal is permanent — never spin on it. Fall back to the - // wallet's own name so the user always ends up with an address. - const permanent = /belongs to another|is taken|reserved|invalid/i.test(e.message); - const own = wallet.nostrNpub && wallet.nostrNpub(); - if (permanent && useReal && own) await claim(own.slice(0, 20), {}); - else if (!permanent) throw e; - } - st = load(); - } + // Claim with the WALLET's own key: it needs no signer, prompts + // nobody, and always works. Tying the address to the user's real + // npub happens at login instead (adoptIdentity below), where a + // signer is already live — an address must never wait on a popup. + const own = wallet.nostrNpub && wallet.nostrNpub(); + if (own) { await claim(own.slice(0, 20), {}); st = load(); } } checked = true; clearTimeout(deadline); deadline = null; @@ -145,12 +133,25 @@ export function namesFeature(ctx) { if (uri && uri !== st.uri) await claim(st.name); } catch (e) { console.warn('names: refresh failed —', e.message); - if (attempt < 6) { retryTimer = setTimeout(() => refresh(attempt + 1), 5000); return; } + lastError = e.message; + if (attempt < 3) { retryTimer = setTimeout(() => refresh(attempt + 1), 4000); return; } checked = true; render(); } } + // Called right after a nostr login, while that signer is definitely live: + // move the payment address onto the user's real npub and nominate the + // wallet key to keep the record updated afterwards. + async function adoptIdentity(signer, npub) { + if (!signer || !npub || !available()) return null; + const name = npub.slice(0, 20); + if (load().name === name) return name; + await claim(name, { signer, manager: wallet.nostrPubkey() }); + render(); + return name; + } + // ---- sending to a name ------------------------------------------------- function beginResolve(text) { @@ -269,6 +270,7 @@ export function namesFeature(ctx) { if (!st.name) { return h('div', { class: 'card col', style: 'gap:10px' }, seg, + lastError ? h('div', { class: 'notice err small' }, t('namesAutoFailed', { why: lastError })) : null, h('p', { class: 'small muted', style: 'margin:0' }, t('namesDesc')), claimForm(false)); } @@ -282,7 +284,8 @@ export function namesFeature(ctx) { return { id: 'names', - init() { checked = false; refresh(); }, + init() { checked = false; lastError = null; refresh(); }, + namesAdoptIdentity(signer, npub) { return adoptIdentity(signer, npub); }, stop() { clearTimeout(retryTimer); clearTimeout(deadline); deadline = null; }, receiveModes() { if (!available()) return []; diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index cea13b4..7d4bdc1 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -52,6 +52,8 @@ export function nostrLoginFeature(ctx) { live = signer; await ctx.openMnemonic(res.mnemonic, res.passphrase || '', { nostrPubkey: signer.pubkey }); save({ ...load(), pubkey: signer.pubkey, linked: Date.now() }); + // claim the real npub as the payment address while this signer is live + ctx.hook('namesAdoptIdentity', signer, npubOf(signer.pubkey))?.catch?.(() => {}); toast(res.mode === 'derived' ? t('nlOpenedDerived') : t('nlOpenedRestored')); busy(false); } catch (e) { fail(e); } @@ -68,6 +70,7 @@ export function nostrLoginFeature(ctx) { live = st.signer; await ctx.openMnemonic(mnemonic, '', { nostrPubkey: st.signer.pubkey }); save({ ...load(), pubkey: st.signer.pubkey, linked: Date.now() }); + ctx.hook('namesAdoptIdentity', st.signer, npubOf(st.signer.pubkey))?.catch?.(() => {}); toast(t('nlCreated')); busy(false); } catch (e) { fail(e); } @@ -89,6 +92,7 @@ export function nostrLoginFeature(ctx) { await publishWalletBackup(signer, { mnemonic: wallet.mnemonic, passphrase: wallet.passphrase || '' }); live = signer; save({ ...load(), pubkey: signer.pubkey, linked: Date.now() }); + ctx.hook('namesAdoptIdentity', signer, npubOf(signer.pubkey))?.catch?.(() => {}); toast(t('nlLinked')); busy(false); } catch (e) { fail(e); } diff --git a/src/i18n.js b/src/i18n.js index c3684bd..c1722d2 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -449,6 +449,7 @@ const STR = { nlLinked: 'Wallet linked to your Nostr account', nlNeedSeed: 'This wallet has no recovery phrase loaded.', nlAlreadyLinked: 'That Nostr account already has a different wallet.', + namesAutoFailed: 'Couldn’t set your address up automatically ({why}). Pick one below.', namesSettingUp: 'Setting up your payment address…', namesTitle: 'Payment address', receiveNameTab: 'Payment address', -- cgit v1.2.3