diff options
Diffstat (limited to 'src/features/nwc.js')
| -rw-r--r-- | src/features/nwc.js | 29 |
1 files changed, 27 insertions, 2 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js index 73ab781..0b6cf08 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -57,8 +57,28 @@ export function nwcFeature(ctx) { // how long a pay_invoice error (or a sibling's success) waits for the // device that actually paid to speak first errGraceMs = 3000, + claim = claimOn, } = ctx.nwcTransport || {}; + // Cross-device leadership for a pay request. Every awake device of the + // wallet hears every request within the same second; the tab lock only + // covers one browser profile, and two profiles (or two machines) each + // locked an HTLC for the same zap. The notifier hands each request to the + // first device that asks. Unreachable notifier → answer anyway: a lone + // device must never go silent over a hiccup here. + const claimAs = Math.random().toString(36).slice(2, 12); + async function claimOn(id) { + try { + const r = await fetch(`${NOTIFIER}/claim`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ event: id, by: claimAs }), + signal: AbortSignal.timeout ? AbortSignal.timeout(4000) : undefined, + }); + if (!r.ok) return true; + return (await r.json()).ok !== false; + } catch { return true; } + } + // ---- persisted connections ------------------------------------------- // { id, name, secret (client sk hex), clientPk, servicePk, serviceSk, // maxSat, dailySat, spentToday, spentDate, created, lastUsed, revoked } @@ -350,7 +370,8 @@ export function nwcFeature(ctx) { await reply(c, ev, scheme, payload); }; try { - const out = await handle(c, method, params); + const out = await handle(c, method, params, () => claim(ev.id)); + if (out && out.silent) return console.log('nwc: standing down — another device claimed', ev.id.slice(0, 8)); await sendChecked({ result_type: method, ...out }); } catch (e) { await sendChecked({ result_type: method, ...errRes('INTERNAL', e.message || 'failed') }); @@ -358,7 +379,7 @@ export function nwcFeature(ctx) { render(); } - async function handle(c, method, params) { + async function handle(c, method, params, claimReq) { if (!hook('arkReady')) return errRes('UNAUTHORIZED', 'Ark is not configured in this wallet'); if (method === 'get_info') { @@ -398,6 +419,10 @@ export function nwcFeature(ctx) { if (dec.amountSat > left) { return errRes('QUOTA_EXCEEDED', `over the remaining daily budget (${left} sat)`); } + // Everything local has passed; now make sure this device is the one + // that pays. Claiming before the checks would block a sibling behind + // a refusal it would not itself have given. + if (claimReq && !(await claimReq())) return { silent: true }; let res; try { res = await hook('arkPayInvoice', invoice, { maxAmountSat: c.maxSat }); |
