diff options
| author | Adam Soltys <[email protected]> | 2026-08-01 09:25:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-01 09:25:23 -0700 |
| commit | e09b76ea0ed13840426a56f031769f8d4154f34b (patch) | |
| tree | 8a1bdad05d4ab65f795673ba96c532abc0fd5944 /src/features/ark.js | |
| parent | 4f7cfb911a12c2447a9cd18a83f0e806912db964 (diff) | |
| download | coinosv3-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.js | 47 |
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 |
