diff options
Diffstat (limited to 'src/features/nwc.js')
| -rw-r--r-- | src/features/nwc.js | 56 |
1 files changed, 46 insertions, 10 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js index 9c71a7f..9768740 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -30,7 +30,7 @@ import { t } from '../i18n.js'; import { dlog } from '../debug.js'; import { qrSvg } from '../qr.js'; import { getNetwork } from '../api.js'; -import { loadBg, saveBg, clearBg } from '../nwc-bg.js'; +import { loadBg, saveBg, clearBg, readBgLog } from '../nwc-bg.js'; import { encodeNoffer } from '../noffer.js'; import { maybeBolt11 } from '../ark/lightning.js'; import { withRequestLock } from '../nwc-lock.js'; @@ -56,7 +56,7 @@ const nowSec = () => Math.floor(Date.now() / 1000); const errRes = (code, message) => ({ error: { code, message } }); export function nwcFeature(ctx) { - const { h, ui, render, wallet, hook, fmtAmount, unitLabel, copyBtn, toast } = ctx; + const { h, ui, render, wallet, hook, fmtAmount, unitLabel, copyBtn, copy, toast } = ctx; // Relay transport, injectable so the protocol can be driven in tests // without a live relay. const { @@ -388,7 +388,10 @@ export function nwcFeature(ctx) { return !!(prior && prior.length); }; if (age > 5 && await lookBack()) return dlog('nwc: skip — request already answered elsewhere', ev.id.slice(0, 8)); - if (mobile && age <= 5) await new Promise((r) => setTimeout(r, 1000)); + // (a phone with this app on screen is as awake as any computer: only + // one behind another app holds back — every zap answered by an open + // phone used to wait this second for nobody) + if (mobile && age <= 5 && typeof document !== 'undefined' && document.hidden) await new Promise((r) => setTimeout(r, 1000)); claimed = await claim(ev.id); if (claimed === false) return dlog('nwc: standing down — another device claimed', ev.id.slice(0, 8)); if (claimed === null && age <= 5) { @@ -478,11 +481,21 @@ export function nwcFeature(ctx) { try { res = await hook('arkPayInvoice', invoice, { maxAmountSat: c.maxSat }); } catch (e) { - // The ASP already holds a payment for this invoice: another device - // of this wallet took the same request first. Its outcome is ours — - // wait for it rather than report a failure over a zap that is - // paying. The payer records the spend; this side only relays. - if (!/already in progress/i.test(e.message || '')) throw e; + // A payment for this invoice already exists: ours, begun on an + // earlier request (the worker started it while closed, say), or + // another device's — the ASP refuses a second HTLC for one invoice. + // Its outcome is the answer, never an error whose text says "paid": + // a client once read "already paid or is being paid" as paid, hours + // before the money moved. The payer records the spend; this side + // only relays. + if (!/already in progress|already paid/i.test(e.message || '')) throw e; + const mine = await hook('arkLnLookup', { paymentHash: dec.paymentHash }).catch(() => null); + if (mine && mine.type === 'outgoing' && mine.state === 'settled' && mine.preimage) { + return { sibling: true, result: { preimage: mine.preimage, fees_paid: 0 } }; + } + if (mine && mine.type === 'outgoing' && mine.state === 'pending') { + return errRes('OTHER', 'a payment for this invoice is still in progress; ask again in a moment'); + } const out = await hook('arkLnOutcome', dec.paymentHash, 60000); if (!out || out.status !== 'success') throw e; return { sibling: true, result: { preimage: out.preimage, fees_paid: 0 } }; @@ -661,6 +674,10 @@ export function nwcFeature(ctx) { { kinds: [OFFER_KIND], '#p': [o.pk], since: nowSec() - MAX_AGE_SEC }, (ev) => { onOfferRequest(ev).catch((e) => console.warn('nwc: offer handler', e.message)); }, )); + // a beat for the subscription to reach the relays, then tell the + // registrar this wallet is here to answer: Lightning deliveries + // queued for a wallet on another ark server go out now (names) + setTimeout(() => { if (gen === listenGen) Promise.resolve(hook('namesListening')).catch(() => {}); }, 1500); } const list = [...conns(), ...revokedConns()]; dlog(`nwc: listen() gen ${gen} — ${list.length} connection(s)`); @@ -967,7 +984,14 @@ export function nwcFeature(ctx) { if (typeof window !== 'undefined') { window.addEventListener('online', () => listen()); document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible' && conns().length) { listen(); refreshRegistration(); } + if (document.visibilityState === 'visible' && conns().length) { + listen(); refreshRegistration(); + // the worker may have paid while this app sat behind another one + Promise.resolve(hook('arkBgAbsorb')).then((had) => { if (had) return reconcileBg(); }).catch(() => {}); + } + // going behind another app: the worker answers from the mirror now, so + // it gets this moment's coins, not the last minute-and-a-half's + if (document.visibilityState === 'hidden' && conns().length) writeBg(); }); } } @@ -1093,7 +1117,19 @@ export function nwcFeature(ctx) { h('button', { class: 'btn-sm', onClick: () => { const s = load(); s.paySentNotify = !on; save(s); writeBg(); render(); } }, on ? t('nwcOn') : t('nwcOff'))), - h('div', { class: 'small faint' }, t('notifPaySentHint')))]; + h('div', { class: 'small faint' }, t('notifPaySentHint'))), + // the worker's diary, for the report that says where a closed wallet + // got to (a phone stops it without a trace) + h('div', { class: 'card col', style: 'gap:10px' }, + h('div', { class: 'row between' }, + h('span', { class: 'small' }, t('nwcWorkerLog')), + h('button', { class: 'btn-sm', onClick: async () => { + const lines = await readBgLog(); + if (!lines.length) { toast(t('nwcWorkerLogEmpty')); return; } + await copy(lines.join('\n')); + toast(t('copied')); + } }, t('copy'))), + h('div', { class: 'small faint' }, t('nwcWorkerLogHint')))]; }, }; } |
