summaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 09:25:23 -0700
committerAdam Soltys <[email protected]>2026-08-01 09:25:23 -0700
commite09b76ea0ed13840426a56f031769f8d4154f34b (patch)
tree8a1bdad05d4ab65f795673ba96c532abc0fd5944 /src/features/ark.js
parent4f7cfb911a12c2447a9cd18a83f0e806912db964 (diff)
downloadcoinosv3-e09b76ea0ed13840426a56f031769f8d4154f34b.tar.gz
coinosv3-e09b76ea0ed13840426a56f031769f8d4154f34b.zip
Add Nostr Wallet Connect (NIP-47) — hal as a wallet service
Other apps can connect over nostr and pay from this wallet without touching its keys, within a per-payment and daily budget the user sets, revocable at any time. hal holds no native Lightning balance, so every operation runs over Ark: pay_invoice takes the swap bridge when configured and falls back to the ASP's lightning-send, make_invoice mints on the ASP. NWC availability therefore tracks Ark availability. - kinds 13194/23194/23195, nip44_v2 and nip04, replying in whichever scheme the request used; every request gets a reply so clients never hang - each connection gets its own service keypair, so revoking one can't affect the others and none is tied to the user's nostr identity - budgets are checked BEFORE spending, not reconciled after - requests are ignored unless signed by that connection's client key, and replayed/stale events (>120s) can't re-trigger a payment - Settings card to create, show (URI + QR) and revoke connections, and it says plainly that this only works while a tab is open tools/nwc-test.js drives it as a real client: both encryption schemes, budget refusal with a genuine over-limit invoice, unsupported-method errors, and a request from an unauthorized key being ignored. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/features/ark.js')
-rw-r--r--src/features/ark.js47
1 files changed, 47 insertions, 0 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 56ffc4c..18baef6 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -543,6 +543,43 @@ export function arkFeature(ctx) {
for (const rec of open) await tryBridgeRefund(rec).catch(() => {});
}
+ // Pay a bolt11 with no UI at all — used by the NWC wallet service. Same
+ // routing as the interactive path: the cheaper bridge first, the ASP's own
+ // lightning-send as fallback. Resolves { preimage, feeSat, amountSat }.
+ async function payInvoiceHeadless(invoice, { maxAmountSat } = {}) {
+ const dec = maybeBolt11(invoice);
+ if (!dec) throw new Error('not a bolt11 invoice');
+ if (!dec.amountSat) throw new Error('zero-amount invoices are not supported');
+ if (maxAmountSat && dec.amountSat > maxAmountSat) {
+ throw new Error(`amount ${dec.amountSat} exceeds the limit of ${maxAmountSat} sat`);
+ }
+ const mgr = await connectArk();
+
+ // 1. the bridge, when one is configured and can take it
+ try {
+ const q = await bridgeQuote(invoice);
+ if (q && mgr.vtxos().some((v) => v.state === 'spendable' && v.amountSat >= q.totalSat)) {
+ const res = await bridgePay(q, invoice);
+ return { preimage: res.preimage, feeSat: q.feeSat, amountSat: q.amountSat };
+ }
+ } catch (e) {
+ console.warn('nwc: bridge pay failed, falling back to the ASP:', e.message);
+ }
+
+ // 2. the ASP's native lightning-send
+ const id = await mgr.payLnInvoice(invoice);
+ for (let i = 0; i < 60; i++) {
+ const a = mgr.lnAction(id);
+ if (!a || a.step === 'done') break;
+ if (a.step === 'failed') throw new Error(a.error || 'payment failed');
+ await new Promise((r) => setTimeout(r, 2000));
+ await mgr.driveLn(id).catch(() => {});
+ }
+ const a = mgr.lnAction(id);
+ if (!a || a.step !== 'done') throw new Error(a?.error || a?.lastError || 'payment did not complete');
+ return { preimage: a.preimage, feeSat: a.feeSat, amountSat: a.amountSat };
+ }
+
// A bolt11 lands in Send (via the swaps feature's delegation hook, or
// directly when swaps is absent): take over when the ark balance can
// plausibly cover it, else decline so the Boltz path handles it.
@@ -1614,6 +1651,16 @@ export function arkFeature(ctx) {
// swaps feature's delegation (its matcher runs first) or directly in
// builds without the swaps feature.
startArkLnPay(invoice, meta) { return startArkLnPay(invoice, meta); },
+ // ---- headless seam for the NWC wallet service ----
+ arkPayInvoice(invoice, opts) { return payInvoiceHeadless(invoice, opts); },
+ arkSpendableSat() { const b = arkBalance(); return b ? b.spendableSat : 0; },
+ arkReady() { return arkAvailable(); },
+ async arkMakeInvoice(amountSat, description) {
+ const mgr = await connectArk();
+ const a = await mgr.createLnInvoice(amountSat, description);
+ return { invoice: a.invoice, paymentHash: a.paymentHash, amountSat };
+ },
+ arkMovements() { const s = arkStateNow(); return s ? (s.movements || []) : []; },
// An npub pasted into Send becomes an ark zap (needs the nostr seam from
// the sync feature and a connected-able ark). A bolt11 is handled here
// only in builds without the swaps feature (whose matcher runs first and