diff options
Diffstat (limited to 'src/nwc-respond.js')
| -rw-r--r-- | src/nwc-respond.js | 40 |
1 files changed, 32 insertions, 8 deletions
diff --git a/src/nwc-respond.js b/src/nwc-respond.js index 0778813..6ea23ab 100644 --- a/src/nwc-respond.js +++ b/src/nwc-respond.js @@ -24,6 +24,13 @@ import { budgetRenewsAt, renewalPeriod, spentInWindow } from './nwc-budget.js'; import { renewPolicy } from './ark/manager.js'; const MAX_AGE_SEC = 120; +// A request the worker can't act on (keys or coins short in the mirror, an +// old mirror) wakes the user — and, unless someone answers first, tells the +// other app so (PullCap read the silence as "no answer" and suspended the +// subscription; with an error it can tell the payer their wallet is waiting +// for them). The notifier holds the reply this long before sending it. +const DECLINE_AFTER_MS = 20_000; +const WAIT_MSG = 'Coinos is waiting for its owner to open the app and approve. Look this request up again later.'; const PAY_WAIT_MS = 20_000; // inside the SW's ~30s budget, with reply margin const nowSec = () => Math.floor(Date.now() / 1000); const errRes = (t, code, message) => ({ result_type: t, error: { code, message } }); @@ -206,7 +213,10 @@ export async function respondFromBg(data, { } = {}) { const ev = data && data.event; if (!ev || !ev.id || !ev.pubkey) return false; - if (nowSec() - (ev.created_at || 0) > MAX_AGE_SEC) return false; + // Older than the request window (a push that sat at APNs while the phone + // was out of reach): the other app has moved on — nothing to answer, and + // no "Payment request" to show for it. The wake still tends what's in flight. + if (nowSec() - (ev.created_at || 0) > MAX_AGE_SEC) { log(`request ${String(ev.id).slice(0, 8)} is ${nowSec() - ev.created_at} s old — ignored`); return true; } const target = (ev.tags?.find((t) => t[0] === 'p') || [])[1]; if (!target) return false; @@ -250,6 +260,20 @@ export async function respondFromBg(data, { try { req = JSON.parse(await decrypt(scheme, conn.serviceSk, ev.pubkey, ev.content)); } catch { return false; } const method = req?.method; const params = req?.params || {}; + // Wake the user, out loud: the wait reply is left with the notifier, which + // sends it only if nobody (a sibling device, the woken user) answers in + // time — so it can never land on top of a real payment. + const declineLater = async (why) => { + log(why + ' — waking the user; the app is told to wait'); + try { + const evt = await buildReply(conn, ev, scheme, errRes(method || 'error', 'OTHER', WAIT_MSG)); + await fetchFn(`${notifier}/decline`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ event: evt, afterMs: DECLINE_AFTER_MS }), + }); + } catch (e) { log('could not leave the wait reply: ' + (e && e.message)); } + return false; + }; // the push's budget (on iOS about ten seconds from the push, not thirty) const deadline = until || Date.now() + PAY_WAIT_MS; let lookupNotice = null; @@ -301,9 +325,9 @@ export async function respondFromBg(data, { if (method === 'make_invoice') { const sat = Math.floor((params.amount || 0) / 1000); if (!sat) { await publish(errRes('make_invoice', 'OTHER', 'amount required (msat)')); return true; } - if (!rec.keys5 || !rec.key4 || !rec.mgr) return false; // old mirror: wake the user - if (!rec.keys5[String(rec.mgr.nextLnRecvIndex || 0)]) return false; // preimage window exhausted - if (!rec.keys[String(rec.mgr.nextKeyIndex || 1)]) return false; // claim key window exhausted + if (!rec.keys5 || !rec.key4 || !rec.mgr) return declineLater('old mirror, no receive keys'); + if (!rec.keys5[String(rec.mgr.nextLnRecvIndex || 0)]) return declineLater('preimage window exhausted'); + if (!rec.keys[String(rec.mgr.nextKeyIndex || 1)]) return declineLater('key window exhausted'); let a; try { const mgr = await bgManager(rec, walletKey, saveFn); @@ -340,7 +364,7 @@ export async function respondFromBg(data, { tx = view.lnLookup(q); if (tx) { ({ walletKey, rec, conn } = m); break; } } - if (!tx && !rec.mgr) return false; + if (!tx && !rec.mgr) return declineLater('old mirror, no state to look up'); // a payment of ours still on its way (incoming: claim it; outgoing: a // send an earlier wake began — drive it on, so the lookup itself can // answer 'settled' with the preimage instead of a second pay request) @@ -512,13 +536,13 @@ export async function respondFromBg(data, { const withNotices = (out) => (notices.length && out ? { ...(out === true ? {} : out), notices } : out); const spendable = spendableSat(rec); - if (spendable < dec.amountSat) { log(`${spendable} sat spendable in the mirror, ${dec.amountSat} asked — waking the user`); return false; } + if (spendable < dec.amountSat) return declineLater(`${spendable} sat spendable in the mirror, ${dec.amountSat} asked`); // The ASP's own Lightning send, over the mirrored state. Change and HTLC // outputs allocate from nextKeyIndex, which must stay inside the key // window the app pre-derived; if it ran out, wake the user instead. - if (!rec.keys[String(rec.mgr.nextKeyIndex || 1)]) { log('key window exhausted'); return false; } - try { await ready(); } catch (e) { log('ark init failed: ' + e.message); return false; } + if (!rec.keys[String(rec.mgr.nextKeyIndex || 1)]) return declineLater('key window exhausted'); + try { await ready(); } catch (e) { return declineLater('ark init failed: ' + e.message); } if (!(await claim())) { log('another device claimed this request — standing down'); return true; } // whose send this is, for the wake that finishes it should this one not |
