aboutsummaryrefslogtreecommitdiffstats
path: root/src/nostr-login.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-04 01:06:24 -0700
committerAdam Soltys <[email protected]>2026-08-04 01:06:24 -0700
commit971db911d47af391695a9d83a3d31c089bfee782 (patch)
treebe7648ca49620c27686542f5baf87347f1cd8164 /src/nostr-login.js
parent2e4432e89091799598d320f82b2c05bb4d2d9d82 (diff)
downloadcoinosv3-971db911d47af391695a9d83a3d31c089bfee782.tar.gz
coinosv3-971db911d47af391695a9d83a3d31c089bfee782.zip
Never mistake a failed backup lookup for 'no wallet yet'
Signing in with a key whose wallet backup exists on relays could still land in 'create a new wallet' when the lookup failed (relays unreachable, signer decrypt denied) — and confirming would have replaced the real backup on any relay the new one reached. Absence is now only believed when a relay actually answered; decrypt failures surface as errors; and the create path takes one last look and adopts a backup that surfaces rather than clobbering it. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/nostr-login.js')
-rw-r--r--src/nostr-login.js26
1 files changed, 20 insertions, 6 deletions
diff --git a/src/nostr-login.js b/src/nostr-login.js
index ec18942..301bbeb 100644
--- a/src/nostr-login.js
+++ b/src/nostr-login.js
@@ -187,16 +187,30 @@ export function keySigner(sk) {
// Look for a wallet this nostr account already published. Returns the
// mnemonic, or null when the account has no wallet yet.
+//
+// "No wallet" is a claim, not a default: acting on it mints a NEW wallet
+// whose backup would replace this one on every relay it reaches. So absence
+// is only believed when at least one relay actually answered, and a backup
+// we can see but not decrypt is an error, never a shrug — each query is
+// per-relay because a pooled query swallows individual connection failures.
export async function fetchWalletBackup(signer, relays = BACKUP_RELAYS) {
const pool = new SimplePool();
+ const filter = { kinds: [BACKUP_KIND], authors: [signer.pubkey], '#d': [SEED_DTAG] };
try {
- const evs = await pool.querySync(relays,
- { kinds: [BACKUP_KIND], authors: [signer.pubkey], '#d': [SEED_DTAG] }, { maxWait: 6000 });
- const newest = evs.sort((a, b) => b.created_at - a.created_at)[0];
+ const results = await Promise.allSettled(relays.map(async (url) => {
+ await pool.ensureRelay(url, { connectionTimeout: 5000 });
+ return pool.querySync([url], filter, { maxWait: 6000 });
+ }));
+ const answered = results.filter((r) => r.status === 'fulfilled');
+ if (!answered.length) throw new Error('could not reach any relay to look for an existing wallet — check your connection and try again');
+ const newest = answered.flatMap((r) => r.value).sort((a, b) => b.created_at - a.created_at)[0];
if (!newest) return null;
- const body = JSON.parse(await signer.decryptSelf(newest.content));
- return body && body.mnemonic ? body : null;
- } catch { return null; } finally { try { pool.close(relays); } catch {} }
+ let body;
+ try { body = JSON.parse(await signer.decryptSelf(newest.content)); }
+ catch { throw new Error('found this account’s wallet but could not decrypt it — approve the decryption prompt in your signer and try again'); }
+ if (!body || !body.mnemonic) throw new Error('found this account’s wallet backup but it is unreadable');
+ return body;
+ } finally { try { pool.close(relays); } catch {} }
}
// Publish (or replace) the encrypted wallet association for this account.