diff options
| author | Adam Soltys <[email protected]> | 2026-08-29 00:50:09 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-29 00:50:09 -0700 |
| commit | 2023fe06d5cc4867f09d5916680b5fe92394dacb (patch) | |
| tree | af8e230bd283776273537da60fe9c0d6782cba28 /src/ark/manager.js | |
| parent | 8ea0416c08535750839088c060d3252415510cc5 (diff) | |
| download | coinosv3-2023fe06d5cc4867f09d5916680b5fe92394dacb.tar.gz coinosv3-2023fe06d5cc4867f09d5916680b5fe92394dacb.zip | |
The wallet learns to read BOLT 12 for itself: offers and invoices decode client-side, and every invoice the ASP fetches over onion messages is verified here — payment hash read from the raw TLVs, offer terms mirrored byte-for-byte, the issuer's schnorr signature checked against LDK's exact merkle construction (proven against a live CLN-signed fixture) — so no server can hand back an invoice whose preimage it already holds. Paste an lno1 or resolve a name whose record carries one and the existing HTLC pay flow carries it (captaind's Invoice enum takes lni strings natively); a name's owner may now publish their OWN node's offer in their DNS record, retiring the registrar's stand-in — senders fetch invoices straight from their node and no coinos service ever touches the money. The /lnquote fallback prices blinded paths by entry node + advertised payinfo
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
Diffstat (limited to 'src/ark/manager.js')
| -rw-r--r-- | src/ark/manager.js | 32 |
1 files changed, 27 insertions, 5 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 50214fc..2a6c956 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -29,7 +29,7 @@ import { genUserNonces, cosignPartBytes, combineCosign, } from './send.js'; import { - decodeBolt11, lnSendFee, lnReceiveFee, fetchLnRouteFee, + decodeBolt11, decodeLnInvoice, fetchBolt12Invoice, lnSendFee, lnReceiveFee, fetchLnRouteFee, requestLightningPayHtlcCosign, initiateLightningPayment, checkLightningPayment, requestLightningPayHtlcRevocation, startLightningReceive, checkLightningReceive, prepareLightningReceiveClaim, claimLightningReceive, cancelLightningReceive, @@ -51,6 +51,7 @@ import { } from './offboard.js'; import { signedExitTxs } from './exit.js'; import { validateVtxo, VtxoValidationError } from './validate.js'; +import { decodeOffer, decodeBolt12Invoice, verifyOfferInvoice, encodeBech32Raw } from '../bolt12.js'; const EMPTY_STATE = () => ({ v: 1, @@ -787,12 +788,33 @@ export class ArkManager { catch { return Math.max(3, Math.ceil(amountSat / 1000)); } } - // Pay a bolt11 invoice with ark funds. Returns the action id; drive to a - // terminal step ('done' | 'failed') via driveLn()/sync. + // Fetch a BOLT 12 invoice for an offer through the server's CLN, then + // verify it HERE: hash and amount are read out of the raw TLVs and the + // issuer's signature is checked, so the server can't hand back an invoice + // whose preimage it already holds. Returns the lni string payLnInvoice + // (and captaind) accept. + async fetchBolt12(offerStr, amountSat) { + const offer = decodeOffer(offerStr); + if (offer.currency) throw new Error(`offer is priced in ${offer.currency}, not sats`); + const bytes = await fetchBolt12Invoice(this.arkUrl, offer.bytes, amountSat); + const inv = decodeBolt12Invoice(bytes); + verifyOfferInvoice(offer, inv); + if (amountSat && inv.amountSat !== amountSat) throw new Error('invoice amount does not match'); + if (inv.expiresAt && inv.expiresAt < Date.now() + 60_000) throw new Error('invoice already expired'); + return { + invoice: encodeBech32Raw('lni', bytes), + amountSat: inv.amountSat, paymentHash: inv.paymentHash, description: inv.description, + }; + } + + // Pay a bolt11 or bolt12 invoice with ark funds. Returns the action id; + // drive to a terminal step ('done' | 'failed') via driveLn()/sync. async payLnInvoice(invoice, { amountSat: userAmountSat, routingFeeSat } = {}) { - const dec = decodeBolt11(invoice); + const dec = decodeLnInvoice(invoice); const expectNet = { bitcoin: 'mainnet', regtest: 'regtest', signet: 'signet', testnet: 'testnet' }[this.info.network]; - if (expectNet && dec.network !== expectNet) throw new Error(`invoice is for ${dec.network}, wallet is on ${expectNet}`); + // a null network is a bolt12 on a chain we don't recognize (custom + // signets) — the server rejects a true mismatch, so don't guess here + if (expectNet && dec.network && dec.network !== expectNet) throw new Error(`invoice is for ${dec.network}, wallet is on ${expectNet}`); const amountSat = dec.amountSat ?? userAmountSat; if (!amountSat || amountSat <= 0) throw new Error('invoice has no amount'); if (this.state.actions.some((a) => |
