aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 22:42:15 -0700
committerAdam Soltys <[email protected]>2026-08-01 22:42:15 -0700
commit94f8f449bdce9ee66424a282ced51e2cfaddca93 (patch)
treee722906c5850560d261e7559476ed1c75b879b6d /src/features
parent39067a27e0ce24386aa6f08c78e9a916e1c8a202 (diff)
downloadcoinosv3-94f8f449bdce9ee66424a282ced51e2cfaddca93.tar.gz
coinosv3-94f8f449bdce9ee66424a282ced51e2cfaddca93.zip
The real cause: the CORS preflight rejected our own auth header
NIP-98 puts the claim under an Authorization header, but the registrar only allowed content-type in access-control-allow-headers, so browsers blocked every claim before sending it. The failure arrived as a generic network error, which the retry logic treated as transient — hence the endless 'setting up your payment address' spinner. Bun tests never saw it because they don't preflight. Registrar now allows the header. Client-side, nothing may hang: signing, the registrar call and the ark lookup all have timeouts, retries are capped, and a one-minute watchdog always resolves the spinner into either an address or the manual form. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/features')
-rw-r--r--src/features/names.js29
1 files changed, 21 insertions, 8 deletions
diff --git a/src/features/names.js b/src/features/names.js
index d430d98..48fb72a 100644
--- a/src/features/names.js
+++ b/src/features/names.js
@@ -32,13 +32,19 @@ export function namesFeature(ctx) {
signEvent: async (e) => wallet.nostrSign(e),
});
+ // Nothing here may hang: a stuck signer prompt or a dead endpoint must
+ // surface as an error, not as a spinner that never stops.
+ const withTimeout = (p, ms, what) => Promise.race([
+ p, new Promise((_, rej) => setTimeout(() => rej(new Error(`${what} timed out`)), ms)),
+ ]);
+
async function post(path, method, payload, signer) {
const url = `${REGISTRAR}${path}`;
const body = JSON.stringify(payload);
- const auth = await nip98Header(signer || walletSigner(), url, method, body);
- const r = await fetch(url, {
+ const auth = await withTimeout(nip98Header(signer || walletSigner(), url, method, body), 20000, 'signing');
+ const r = await withTimeout(fetch(url, {
method, headers: { 'content-type': 'application/json', authorization: auth }, body,
- });
+ }), 20000, 'registrar');
const j = await r.json().catch(() => ({}));
if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`);
return j;
@@ -51,7 +57,7 @@ export function namesFeature(ctx) {
&& !!wallet.nostrSign && !!hook('arkReady');
async function currentUri() {
- const addr = await hook('arkStaticAddress');
+ const addr = await withTimeout(hook('arkStaticAddress'), 20000, 'ark');
return addr ? `bitcoin:?ark=${encodeURIComponent(addr)}` : null;
}
@@ -84,8 +90,14 @@ export function namesFeature(ctx) {
// an optional change (and may cost something one day, to keep squatters
// out), so nothing here ever blocks the wallet.
let retryTimer = null;
+ let deadline = 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);
+ }
try {
if (!available()) {
// Ark connects lazily AFTER the wallet opens, so the first pass
@@ -107,7 +119,7 @@ export function namesFeature(ctx) {
// ever fail.
const linked = hook('nostrLoginIdentity');
let signer = linked && linked.signer;
- if (linked && !signer) { try { signer = await hook('nostrLoginResume'); } catch {} }
+ 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) {
@@ -126,13 +138,14 @@ export function namesFeature(ctx) {
}
}
checked = true;
+ clearTimeout(deadline); deadline = null;
render();
if (!st.name) return;
const uri = await currentUri();
if (uri && uri !== st.uri) await claim(st.name);
} catch (e) {
- console.warn('names: refresh failed', e.message);
- if (attempt < 10) { retryTimer = setTimeout(() => refresh(attempt + 1), 5000); return; }
+ console.warn('names: refresh failed —', e.message);
+ if (attempt < 6) { retryTimer = setTimeout(() => refresh(attempt + 1), 5000); return; }
checked = true;
render();
}
@@ -270,7 +283,7 @@ export function namesFeature(ctx) {
return {
id: 'names',
init() { checked = false; refresh(); },
- stop() { clearTimeout(retryTimer); },
+ stop() { clearTimeout(retryTimer); clearTimeout(deadline); deadline = null; },
receiveModes() {
if (!available()) return [];
return [{