aboutsummaryrefslogtreecommitdiffstats
path: root/src/ark
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 07:19:29 -0700
committerAdam Soltys <[email protected]>2026-10-02 07:19:29 -0700
commite8d82b8b23c271aa57134d762e14fd6dcbed9c7a (patch)
tree75ce06480f1e65552e43073647b5110ec7e76a63 /src/ark
parentbaef720df9e34be38fc2e5b4e4837674121e5c59 (diff)
downloadcoinosv3-e8d82b8b23c271aa57134d762e14fd6dcbed9c7a.tar.gz
coinosv3-e8d82b8b23c271aa57134d762e14fd6dcbed9c7a.zip
NWC: lookup_invoice, so BTCPay Server can connect
BTCPay's NWC connector requires get_info, make_invoice, lookup_invoice and list_transactions; we never offered lookup_invoice, so it refused the connection string. - ArkManager.lnLookup answers lookup_invoice from wallet state (by hash or bolt11, either direction; pending/settled/expired/failed, preimage once claimed). A retry's several actions on one hash defer to the furthest. - The open app pushes a pending receive along before answering, so a merchant polling sees it settle. - The service worker now answers make_invoice and lookup_invoice too, and claims a paid invoice's HTLCs during the lookup: a BTCPay sale settles with the wallet closed. Its list_transactions reports receives as well. - get_info and the 13194 info event list the new method. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
Diffstat (limited to 'src/ark')
-rw-r--r--src/ark/lightning.js1
-rw-r--r--src/ark/manager.js44
2 files changed, 45 insertions, 0 deletions
diff --git a/src/ark/lightning.js b/src/ark/lightning.js
index 0ac80a5..568d0ab 100644
--- a/src/ark/lightning.js
+++ b/src/ark/lightning.js
@@ -61,6 +61,7 @@ export function decodeBolt11(invoice) {
amountMsat,
amountSat: amountMsat != null ? Number(amountMsat / 1000n) : null,
expiresAt: (ts + expiry) * 1000,
+ createdAt: ts * 1000,
};
}
diff --git a/src/ark/manager.js b/src/ark/manager.js
index 4b65b27..dc80ca3 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -1068,6 +1068,50 @@ export class ArkManager {
return this.driveLn(id);
}
+ // A Lightning payment of this wallet's, either way, as NIP-47
+ // lookup_invoice reports it — found by payment hash or bolt11. null when
+ // the wallet never made or paid that invoice.
+ lnLookup({ paymentHash, invoice } = {}) {
+ const hashOf = (inv) => { try { return decodeBolt11(inv).paymentHash; } catch { return null; } };
+ const hash = String(paymentHash || (invoice && hashOf(invoice)) || '').toLowerCase();
+ if (!/^[0-9a-f]{64}$/.test(hash)) return null;
+ // a retry leaves several actions on one hash: the furthest along speaks
+ const rank = (x) => (x.step === 'done' ? 0 : x.step === 'failed' ? 2 : 1);
+ const a = (this.state.actions || []).filter((x) => (x.type === 'ln-recv' || x.type === 'ln-pay')
+ && (x.paymentHash === hash || (x.invoice && hashOf(x.invoice) === hash)))
+ .sort((x, y) => rank(x) - rank(y))[0];
+ const incoming = a ? a.type === 'ln-recv' : null;
+ const mv = (this.state.movements || []).filter((m) => (incoming == null
+ ? (m.type === 'ln-receive' || m.type === 'ln-send')
+ : m.type === (incoming ? 'ln-receive' : 'ln-send')) && (m.paymentHash === hash || (m.invoice && hashOf(m.invoice) === hash)));
+ const done = mv.find((m) => m.status === 'complete');
+ if (!a && !mv.length) return null;
+ const type = (a ? incoming : mv[0].type === 'ln-receive') ? 'incoming' : 'outgoing';
+ const inv = (a && a.invoice) || mv[0]?.invoice || '';
+ let dec = null; try { dec = inv ? decodeBolt11(inv) : null; } catch {}
+ const settled = (a && a.step === 'done') || !!done;
+ const sec = (ms) => (ms ? Math.floor(ms / 1000) : undefined);
+ let state = 'pending';
+ if (settled) state = 'settled';
+ else if ((a && a.step === 'failed') || (!a && mv.some((m) => m.status === 'failed'))) {
+ state = type === 'incoming' && dec?.expiresAt && Date.now() > dec.expiresAt ? 'expired' : 'failed';
+ } else if (type === 'incoming' && a && a.step === 'awaiting' && dec?.expiresAt && Date.now() > dec.expiresAt) state = 'expired';
+ let preimage = '';
+ if (settled) {
+ if (type === 'outgoing') preimage = (a && a.preimage) || done?.preimage || '';
+ else if (a && Number.isInteger(a.preimageIndex)) { try { preimage = hex.encode(this._lnPreimage(a.preimageIndex)); } catch {} }
+ }
+ const amountSat = (a && a.amountSat) || dec?.amountSat || done?.amountSat || 0;
+ return {
+ type, state, invoice: inv, payment_hash: hash, preimage,
+ description: '', amount: amountSat * 1000,
+ fees_paid: type === 'outgoing' ? ((a && a.feeSat) || 0) * 1000 : 0,
+ created_at: sec(dec?.createdAt || actionTs(a || {}) || mv[0]?.ts) || Math.floor(Date.now() / 1000),
+ expires_at: sec(dec?.expiresAt),
+ settled_at: settled ? sec(done?.ts || actionTs(a || {})) : undefined,
+ };
+ }
+
lnAction(id) {
return this.state.actions.find((a) => a.id === id && a.type.startsWith('ln-'));
}