diff options
Diffstat (limited to 'src/lnurl.js')
| -rw-r--r-- | src/lnurl.js | 34 |
1 files changed, 34 insertions, 0 deletions
diff --git a/src/lnurl.js b/src/lnurl.js index cf851b5..bfebf0d 100644 --- a/src/lnurl.js +++ b/src/lnurl.js @@ -47,6 +47,9 @@ export function lnAddressToUrl(addr) { export function parseZapTarget(input, { pk = null } = {}) { const s = (input || '').trim().replace(/^lightning:/i, ''); if (!s) return null; + // LUD-17: lnurlp:// (pay) and lnurlw:// (withdraw) are the URL itself over https (http on an onion) + const scheme = /^(lnurlp|lnurlw):\/\/(.+)$/i.exec(s); + if (scheme) { const url = (/^[^/]+\.onion(\/|$)/i.test(scheme[2]) ? 'http://' : 'https://') + scheme[2]; return { kind: 'lnurl', url, lnurlBech32: safeEncode(url), pk }; } // An npub/hex pubkey — the caller resolves lud16/lud06 from the profile and // calls fromProfile() below; here we just report the shape. const npk = parseNostrPubkey(s); @@ -96,10 +99,14 @@ async function fetchJson(url, ms = 8000) { } // GET the LNURL-pay params. Normalizes the bits we use; keeps `callback`. +// An lnurl that turns out to be a withdraw (LUD-03) is not an error here: +// the thrown error carries its terms (`withdraw`) for the caller to take up. export async function fetchPayParams(url) { const d = await fetchJson(url); if (!d || d.tag !== 'payRequest' || !d.callback) { if (d && d.status === 'ERROR') throw new Error(d.reason || 'LNURL error'); + const w = withdrawParamsOf(d); + if (w) { const e = new Error('a Lightning withdraw, not a payment'); e.withdraw = w; throw e; } throw new Error('not a Lightning pay endpoint'); } return { @@ -179,3 +186,30 @@ export async function lnurlAuthLogin(auth, key) { return { key: bytesToHex(key.publicKey) }; } +// ---- LNURL-withdraw (LUD-03): the other side asks to PAY us ---- +// Their terms: an amount range (msat), a callback and k1. We make an invoice +// for the amount chosen and hand it to the callback; they pay it. +export function withdrawParamsOf(d) { + if (!d || d.tag !== 'withdrawRequest' || !d.callback || !d.k1) return null; + const min = Math.max(0, Number(d.minWithdrawable) || 0), max = Number(d.maxWithdrawable) || 0; + if (!(max > 0)) return null; + let domain = ''; + try { domain = new URL(d.callback).hostname; } catch {} + return { callback: d.callback, k1: String(d.k1), minMsat: min, maxMsat: Math.max(min, max), description: String(d.defaultDescription || '').slice(0, 200), domain }; +} +export async function fetchWithdrawParams(url) { + const d = await fetchJson(url); + const w = withdrawParamsOf(d); + if (!w) { if (d && d.status === 'ERROR') throw new Error(d.reason || 'LNURL error'); throw new Error('not a Lightning withdraw'); } + return w; +} +// Hand them the invoice to pay. Their OK means they have it, not that it is paid. +export async function submitWithdraw(params, invoice) { + const url = new URL(params.callback); + url.searchParams.set('k1', params.k1); + url.searchParams.set('pr', invoice); + const d = await fetchJson(url.href); + if (!d || String(d.status || '').toUpperCase() !== 'OK') throw new Error((d && d.reason) || 'withdraw refused'); + return true; +} + |
