aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/nwc.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/nwc.js')
-rw-r--r--src/features/nwc.js62
1 files changed, 51 insertions, 11 deletions
diff --git a/src/features/nwc.js b/src/features/nwc.js
index ec79081..a2730f5 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 {
@@ -478,11 +478,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 } };
@@ -822,6 +832,25 @@ export function nwcFeature(ctx) {
// wrong keys — background wake-ups silently die the first time a
// connection is recreated. Debounced; the server replaces by endpoint.
let regTimer = null;
+ // The browser's subscription, made again when it's gone: iOS drops it after
+ // three pushes that showed no notification (and a key rotation strands an
+ // old one), and nothing else re-arms a wallet whose `background` is still on.
+ async function currentSub() {
+ const reg = await navigator.serviceWorker.ready;
+ const { publicKey } = await (await fetch(`${NOTIFIER}/vapid`)).json();
+ const want = b64ToBytes(publicKey);
+ let sub = await reg.pushManager.getSubscription();
+ if (sub) {
+ let have = [];
+ try { have = new Uint8Array(sub.options.applicationServerKey || []); } catch {}
+ if (have.length === want.length && have.every((b, i) => b === want[i])) return sub.toJSON();
+ try { await sub.unsubscribe(); } catch {}
+ }
+ if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return null;
+ sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: want });
+ dlog('nwc: push subscription was gone, made a new one');
+ return sub.toJSON();
+ }
function refreshRegistration() {
if (!load().background) return;
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
@@ -831,8 +860,7 @@ export function nwcFeature(ctx) {
const pks = watchPks();
if (!pks.length) return;
const up2 = upEndpoint();
- const sub = up2 ? { endpoint: up2, unifiedpush: true }
- : await (await navigator.serviceWorker.ready).pushManager.getSubscription().then((x) => x && x.toJSON());
+ const sub = up2 ? { endpoint: up2, unifiedpush: true } : await currentSub();
if (!sub) return;
await fetch(`${NOTIFIER}/register`, {
method: 'POST', headers: { 'content-type': 'application/json' },
@@ -949,7 +977,7 @@ export function nwcFeature(ctx) {
if (typeof window !== 'undefined') {
window.addEventListener('online', () => listen());
document.addEventListener('visibilitychange', () => {
- if (document.visibilityState === 'visible' && conns().length) listen();
+ if (document.visibilityState === 'visible' && conns().length) { listen(); refreshRegistration(); }
});
}
}
@@ -1075,7 +1103,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')))];
},
};
}