diff options
| author | Adam Soltys <[email protected]> | 2026-08-29 00:31:12 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-29 00:31:12 -0700 |
| commit | 8ea0416c08535750839088c060d3252415510cc5 (patch) | |
| tree | 2908f20062ba516464ddfc5489a65082ac7a6b6a /src/clink.js | |
| parent | f06644e729ab5be2fcfa45e32f8b1de52ea36eb3 (diff) | |
| download | coinosv3-8ea0416c08535750839088c060d3252415510cc5.tar.gz coinosv3-8ea0416c08535750839088c060d3252415510cc5.zip | |
An address can point home: a name's owner may register their own CLINK offer code and the registrar asks THEIR node for invoices — delivered there first, float as fallback, and no invoice is relayed or paid unless it demands exactly the sats owed on the right network. hal learns to pay pasted noffer1 codes through the zap flow (fixed, variable and payer-chosen pricing, amount verified before the HTLC locks). The depth advisory slims to one line — an uncooperative exit's price and a Manage link — and the new coins screen shows each vtxo's amount, expiry and arkoor depth beside what renewal costs and buys
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
Diffstat (limited to 'src/clink.js')
| -rw-r--r-- | src/clink.js | 50 |
1 files changed, 50 insertions, 0 deletions
diff --git a/src/clink.js b/src/clink.js new file mode 100644 index 0000000..9f381c1 --- /dev/null +++ b/src/clink.js @@ -0,0 +1,50 @@ +// CLINK payer — ask an offer code's service for an invoice over nostr. +// The counterpart of src/noffer.js (the codec) and the responder in +// features/nwc.js: a NIP-44-encrypted kind-21001 request goes to the service +// pubkey on the relay the code names, and {bolt11} (or {error}) comes back. + +import { + nip44, finalizeEvent, generateSecretKey, publishOn, subscribeOn, +} from './nostr.js'; + +const OFFER_KIND = 21001; + +// dest: a decoded noffer ({ pubkey, relay, offerId }). Signs with a throwaway +// key, so paying reveals no identity to the service. amountSat may be omitted +// for offers that price themselves (the returned invoice carries the amount). +// Resolves the bolt11 string; rejects on a service error or silence. +export function requestOfferInvoice(dest, { amountSat, description, timeoutMs = 12000 } = {}) { + return new Promise((resolve, reject) => { + const relays = [dest.relay].filter(Boolean); + if (!relays.length) return reject(new Error('offer code names no relay')); + const sk = generateSecretKey(); + const key = nip44.getConversationKey(sk, dest.pubkey); + const payload = { offer: dest.offerId || '' }; + if (amountSat) payload.amount_sats = Math.floor(amountSat); + if (description) payload.description = String(description).slice(0, 100); + const req = finalizeEvent({ + kind: OFFER_KIND, created_at: Math.floor(Date.now() / 1000), + tags: [['p', dest.pubkey], ['clink_version', '1']], + content: nip44.encrypt(JSON.stringify(payload), key), + }, sk); + let done = false; + let close = () => {}; + const finish = (err, bolt11) => { + if (done) return; + done = true; + clearTimeout(timer); + close(); + err ? reject(err) : resolve(bolt11); + }; + const timer = setTimeout(() => finish(new Error('the recipient’s service did not answer')), timeoutMs); + // subscribe before publishing — a fast service can answer within the RTT + close = subscribeOn(relays, { kinds: [OFFER_KIND], '#e': [req.id], since: Math.floor(Date.now() / 1000) - 5 }, (ev) => { + try { + const body = JSON.parse(nip44.decrypt(ev.content, key)); + if (body.bolt11) return finish(null, body.bolt11); + if (body.error) return finish(new Error(body.error)); + } catch {} + }); + publishOn(relays, req).catch(() => {}); + }); +} |
