diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 22:41:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 22:41:23 -0700 |
| commit | 9510929a197fc38f1de762d99c8fde80c788ca30 (patch) | |
| tree | 80294c69359a2b382e3b56da803dadd812438863 /src | |
| parent | ccced4634115777fcbedd221f1324612e00b61d5 (diff) | |
| download | coinosv3-9510929a197fc38f1de762d99c8fde80c788ca30.tar.gz coinosv3-9510929a197fc38f1de762d99c8fde80c788ca30.zip | |
Don't keep holding a remote signer that stopped answering
A NIP-46 connection can die quietly, leaving the app with a signer
object that will never reply — and since it's still the cached live
signer, every send after that fails the same way until the page is
reloaded. Wrap a remote signer so the first failed call drops it,
letting the next attempt resume a fresh connection instead.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/nostrlogin.js | 28 |
1 files changed, 24 insertions, 4 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index fa0ad3a..95ab3fe 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -36,6 +36,26 @@ export function nostrLoginFeature(ctx) { let resuming = null; let resumeFailedAt = 0; + // A remote signer's connection can die quietly — the app is left holding a + // signer object that will never answer again, and every send after that + // fails until the page is reloaded. Notice the first failure and drop it, so + // the next attempt resumes a fresh connection instead. + const selfHealing = (signer) => { + if (!signer || signer.kind !== 'bunker') return signer; + const guard = (fn) => async (...args) => { + try { return await fn(...args); } catch (e) { if (live === wrapped) live = null; throw e; } + }; + const wrapped = { + ...signer, + signEvent: guard(signer.signEvent.bind(signer)), + encryptSelf: guard(signer.encryptSelf.bind(signer)), + decryptSelf: guard(signer.decryptSelf.bind(signer)), + encryptTo: guard(signer.encryptTo.bind(signer)), + decryptFrom: guard(signer.decryptFrom.bind(signer)), + }; + return wrapped; + }; + // What it takes to be this identity again after a reload. Without it the app // shows your face in the header but signs everything else with the wallet's // own key — a different pubkey, which is exactly the mismatch people notice @@ -75,7 +95,7 @@ export function nostrLoginFeature(ctx) { // so an extension login later finds this wallet instead of making a new // one. Costs nothing — anyone with the key could derive it anyway. if (res.publish) await publishWalletBackup(signer, { mnemonic: res.mnemonic }).catch(() => {}); - live = signer; + live = selfHealing(signer); await ctx.openMnemonic(res.mnemonic, res.passphrase || '', { nostrPubkey: signer.pubkey }); save({ ...load(), pubkey: signer.pubkey, linked: Date.now(), ...sessionOf(signer) }); // claim the real npub as the payment address while this signer is live @@ -93,7 +113,7 @@ export function nostrLoginFeature(ctx) { const mnemonic = newMnemonic(); await publishWalletBackup(st.signer, { mnemonic }); ui.nostrLoginNew = null; - live = st.signer; + live = selfHealing(st.signer); await ctx.openMnemonic(mnemonic, '', { nostrPubkey: st.signer.pubkey }); save({ ...load(), pubkey: st.signer.pubkey, linked: Date.now(), ...sessionOf(st.signer) }); ctx.hook('namesAdoptIdentity', st.signer, npubOf(st.signer.pubkey))?.catch?.(() => {}); @@ -116,7 +136,7 @@ export function nostrLoginFeature(ctx) { throw new Error(t('nlAlreadyLinked')); } await publishWalletBackup(signer, { mnemonic: wallet.mnemonic, passphrase: wallet.passphrase || '' }); - live = signer; + live = selfHealing(signer); save({ ...load(), pubkey: signer.pubkey, linked: Date.now(), ...sessionOf(signer) }); ctx.hook('namesAdoptIdentity', signer, npubOf(signer.pubkey))?.catch?.(() => {}); toast(t('nlLinked')); @@ -300,7 +320,7 @@ export function nostrLoginFeature(ctx) { if (st.session) { try { const s = await resumeBunker(st.session, { onAuth: (url) => { ui.nostrLoginAuthUrl = url; render(); } }); - if (s && s.pubkey === st.pubkey) return (live = s); + if (s && s.pubkey === st.pubkey) return (live = selfHealing(s)); } catch {} } return null; |
