aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 22:46:19 -0700
committerAdam Soltys <[email protected]>2026-08-01 22:46:19 -0700
commitc8e6de17311e3eaa18a73c2aa0688c80e89db254 (patch)
tree7055365768c8358d45ae99c3496db65babc26100
parent94f8f449bdce9ee66424a282ced51e2cfaddca93 (diff)
downloadcoinosv3-c8e6de17311e3eaa18a73c2aa0688c80e89db254.tar.gz
coinosv3-c8e6de17311e3eaa18a73c2aa0688c80e89db254.zip
Get an address immediately, without waiting on a signer prompt
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 <[email protected]>
-rw-r--r--names/server.js7
-rw-r--r--src/features/names.js53
-rw-r--r--src/features/nostrlogin.js4
-rw-r--r--src/i18n.js1
4 files changed, 38 insertions, 27 deletions
diff --git a/names/server.js b/names/server.js
index 8f1971a..903fa68 100644
--- a/names/server.js
+++ b/names/server.js
@@ -242,7 +242,9 @@ function rateOk(ip, limit = 10) {
// HTTP
// ---------------------------------------------------------------------------
-const json = (body, status = 200) => new Response(JSON.stringify(body), {
+const json = (body, status = 200) => {
+ if (status >= 400) log(`→ ${status} ${JSON.stringify(body).slice(0, 120)}`);
+ return new Response(JSON.stringify(body), {
status,
headers: {
'content-type': 'application/json',
@@ -250,7 +252,8 @@ const json = (body, status = 200) => new Response(JSON.stringify(body), {
'access-control-allow-headers': 'content-type,authorization',
'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS',
},
-});
+ });
+};
Bun.serve({
port: CFG.port || 8798,
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',