@arkade-os/lnurl-client 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1417 @@
1
+ import {
2
+ LnurlError,
3
+ LnurlTimeoutError,
4
+ LnurlTransportError
5
+ } from "./chunk-H7W5RHP2.js";
6
+
7
+ // packages/client/src/http.ts
8
+ async function callFetch(url, init, fetchImpl) {
9
+ try {
10
+ return await fetchImpl(url, init);
11
+ } catch (err) {
12
+ if (err instanceof LnurlError) throw err;
13
+ throw new LnurlTransportError("Request failed", { cause: err });
14
+ }
15
+ }
16
+ async function readBody(response) {
17
+ try {
18
+ return await response.json();
19
+ } catch (err) {
20
+ if (!response.ok) throw new LnurlError(response.statusText, { httpStatus: response.status });
21
+ throw new LnurlTransportError("Request failed", { cause: err });
22
+ }
23
+ }
24
+ function throwForStatus(response, body) {
25
+ const record = typeof body === "object" && body !== null ? body : void 0;
26
+ const ludReason = record?.["status"] === "ERROR" && typeof record?.["reason"] === "string" ? record["reason"] : void 0;
27
+ const apiReason = typeof record?.["error"] === "string" ? record["error"] : void 0;
28
+ const code = typeof record?.["code"] === "string" ? record["code"] : void 0;
29
+ const reason = ludReason ?? apiReason;
30
+ const httpStatus = response.ok ? void 0 : response.status;
31
+ if (reason !== void 0) throw new LnurlError(reason, { httpStatus, code });
32
+ if (!response.ok) throw new LnurlError(apiReason ?? response.statusText, { httpStatus: response.status, code });
33
+ }
34
+ async function fetchJson(url, init, fetchImpl) {
35
+ const response = await callFetch(url, init, fetchImpl);
36
+ const body = await readBody(response);
37
+ throwForStatus(response, body);
38
+ return body;
39
+ }
40
+ function lnurlFetch(url, init, fetchImpl) {
41
+ return fetchJson(url, init, fetchImpl);
42
+ }
43
+ function apiFetch(url, init, fetchImpl) {
44
+ return fetchJson(url, init, fetchImpl);
45
+ }
46
+
47
+ // packages/client/src/encoding.ts
48
+ import { bech32 } from "@scure/base";
49
+ var ADDRESS_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
50
+ function isLnAddress(input) {
51
+ return ADDRESS_PATTERN.test(input);
52
+ }
53
+ var LNURL_BECH32_LIMIT = 1023;
54
+ function decodeLnurl(input) {
55
+ const { prefix, words } = bech32.decode(input, LNURL_BECH32_LIMIT);
56
+ return { prefix, text: new TextDecoder().decode(Uint8Array.from(bech32.fromWords(words))) };
57
+ }
58
+ function isLnUrl(input) {
59
+ try {
60
+ const { prefix, text } = decodeLnurl(input);
61
+ if (prefix !== "lnurl") return false;
62
+ return text.startsWith("https://") || text.startsWith("http://");
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function isValidLnUrl(input) {
68
+ return isLnAddress(input) || isLnUrl(input);
69
+ }
70
+ function toPayRequestUrl(input) {
71
+ if (isLnAddress(input)) {
72
+ const [user, domain] = input.split("@");
73
+ return { url: `https://${domain}/.well-known/lnurlp/${user.toLowerCase()}`, surface: "address" };
74
+ }
75
+ if (isLnUrl(input)) {
76
+ const url = decodeLnurl(input).text;
77
+ const surface = url.includes("/.well-known/lnurlp/") ? "address" : "session";
78
+ return { url, surface };
79
+ }
80
+ throw new LnurlError("Not a valid LNURL or lightning address");
81
+ }
82
+
83
+ // packages/client/src/token-options.ts
84
+ import { base58, createBase58check } from "@scure/base";
85
+ import { sha256 } from "@noble/hashes/sha2.js";
86
+ var CAIP19 = /^([-a-z0-9]{3,8}):([-_a-zA-Z0-9]{1,32})\/([-a-z0-9]{3,8}):([-.%a-zA-Z0-9]{1,128})$/;
87
+ var UNIT_CODE = /^[A-Za-z0-9]{1,16}$/;
88
+ var tronCheck = createBase58check(sha256);
89
+ var decodes = (fn) => {
90
+ try {
91
+ return fn();
92
+ } catch {
93
+ return false;
94
+ }
95
+ };
96
+ var NAMESPACES = {
97
+ eip155: { assetNamespace: "erc20", chain: (r) => /^[1-9][0-9]{0,18}$/.test(r), address: (a) => /^0x[0-9a-fA-F]{40}$/.test(a) },
98
+ solana: {
99
+ assetNamespace: "token",
100
+ chain: () => true,
101
+ address: (a) => decodes(() => /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a) && base58.decode(a).length === 32)
102
+ },
103
+ tron: {
104
+ assetNamespace: "trc20",
105
+ chain: () => true,
106
+ address: (a) => decodes(() => {
107
+ if (!/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(a)) return false;
108
+ const bytes = tronCheck.decode(a);
109
+ return bytes.length === 21 && bytes[0] === 65;
110
+ })
111
+ }
112
+ };
113
+ function isTokenAddress(namespace, address) {
114
+ return NAMESPACES[namespace]?.address(address) ?? false;
115
+ }
116
+ function parseAsset(id) {
117
+ const match = typeof id === "string" ? CAIP19.exec(id) : null;
118
+ if (!match) return void 0;
119
+ const [, namespace, chainReference, assetNamespace, reference] = match;
120
+ const ns = NAMESPACES[namespace];
121
+ if (!ns || ns.assetNamespace !== assetNamespace || !ns.chain(chainReference) || !ns.address(reference)) return void 0;
122
+ return { id, chainId: `${namespace}:${chainReference}`, namespace, chainReference, assetNamespace, reference };
123
+ }
124
+ function unitsOf(payRequest) {
125
+ const out = /* @__PURE__ */ new Map();
126
+ for (const unit of payRequest.units ?? []) {
127
+ const { code, decimals, name } = unit;
128
+ if (typeof code !== "string" || !UNIT_CODE.test(code) || !Number.isInteger(decimals) || decimals < 0 || decimals > 36) continue;
129
+ const key = code.toUpperCase();
130
+ if (out.has(key)) continue;
131
+ out.set(key, { code: key, decimals, ...typeof name === "string" && name.length > 0 && name.length <= 64 ? { name } : {} });
132
+ }
133
+ return out;
134
+ }
135
+ var msat = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
136
+ function tokenOptions(payRequest) {
137
+ const units = unitsOf(payRequest);
138
+ const offered = (payRequest.paymentOptions ?? []).filter((o) => typeof o?.id === "string" && o.id && typeof o.type === "string" && o.type);
139
+ const uses = /* @__PURE__ */ new Map();
140
+ for (const o of offered) uses.set(o.id, (uses.get(o.id) ?? 0) + 1);
141
+ return offered.flatMap((o) => {
142
+ const asset = parseAsset(o.asset);
143
+ const unit = typeof o.unit === "string" ? units.get(o.unit.toUpperCase()) : void 0;
144
+ if (uses.get(o.id) > 1 || !asset || asset.namespace !== o.type || !unit) return [];
145
+ const min = msat(o.minSendable);
146
+ const max = msat(o.maxSendable);
147
+ return [{
148
+ id: o.id,
149
+ asset,
150
+ unit,
151
+ available: o.available !== false,
152
+ ...typeof o.verifiable === "boolean" ? { verifiable: o.verifiable } : {},
153
+ ...min !== void 0 ? { minSendable: min } : {},
154
+ ...max !== void 0 ? { maxSendable: max } : {},
155
+ ...typeof o.provider === "string" && o.provider ? { provider: o.provider } : {}
156
+ }];
157
+ });
158
+ }
159
+
160
+ // packages/client/src/units.ts
161
+ function formatUnitAmount(amount, decimals) {
162
+ const digits = String(amount).padStart(decimals + 1, "0");
163
+ return decimals ? `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}` : digits;
164
+ }
165
+ function parseUnitAmount(text, decimals) {
166
+ const match = /^(\d*)(?:\.(\d*))?$/.exec(text.trim());
167
+ if (!match || match[1] === "" && !match[2]) return void 0;
168
+ const fraction = match[2] ?? "";
169
+ if (fraction.length > decimals) return void 0;
170
+ const amount = Number(`${match[1]}${fraction.padEnd(decimals, "0")}`);
171
+ return Number.isSafeInteger(amount) && amount > 0 ? amount : void 0;
172
+ }
173
+
174
+ // packages/client/src/token-quotes.ts
175
+ var decimal = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
176
+ function tokenPayment(token, answer) {
177
+ if (answer.paymentOption !== token.id) throw new LnurlError(`The callback answered for ${answer.paymentOption}, not ${token.id}`);
178
+ const destination = answer.paymentDestination;
179
+ if (!destination || !isTokenAddress(token.asset.namespace, destination)) {
180
+ throw new LnurlError(`The callback returned no ${token.asset.namespace} address to pay`);
181
+ }
182
+ const payment = answer.paymentQuote?.payment;
183
+ const whole = typeof payment?.amount === "string" && /^[0-9]{1,78}$/.test(payment.amount) && /[1-9]/.test(payment.amount);
184
+ if (!whole || typeof payment?.unit !== "string" || payment.unit.toUpperCase() !== token.unit.code) {
185
+ throw new LnurlError(`The callback quoted no whole amount of ${token.unit.code}`);
186
+ }
187
+ if (!Number.isInteger(token.unit.decimals) || token.unit.decimals < 0 || token.unit.decimals > 36) {
188
+ throw new LnurlError("The token unit has invalid decimals");
189
+ }
190
+ const baseUnits = payment.amount;
191
+ return {
192
+ destination,
193
+ baseUnits,
194
+ amount: decimal(formatUnitAmount(BigInt(baseUnits).toString(), token.unit.decimals)),
195
+ ...answer.paymentDestinationTag !== void 0 ? { paymentDestinationTag: answer.paymentDestinationTag } : {}
196
+ };
197
+ }
198
+ function paysQuote(uri, { destination, baseUnits, amount, paymentDestinationTag }, asset) {
199
+ let url;
200
+ try {
201
+ url = new URL(uri);
202
+ } catch {
203
+ return false;
204
+ }
205
+ if (url.hash) return false;
206
+ const q = url.searchParams;
207
+ const only = (key) => q.getAll(key).length === 1 ? q.get(key) : void 0;
208
+ if (asset.namespace === "eip155" && url.protocol === "ethereum:") {
209
+ const target = /^(0x[0-9a-f]{40})(?:@(\d+))?\/transfer$/i.exec(url.pathname);
210
+ let transferArgumentsOnly = true;
211
+ q.forEach((_value, key) => {
212
+ if (key !== "address" && key !== "uint256") transferArgumentsOnly = false;
213
+ });
214
+ return paymentDestinationTag === void 0 && target !== null && transferArgumentsOnly && target[1].toLowerCase() === asset.reference.toLowerCase() && (target[2] ?? "1") === asset.chainReference && only("address")?.toLowerCase() === destination.toLowerCase() && only("uint256") === baseUnits;
215
+ }
216
+ if (asset.namespace === "solana" && url.protocol === "solana:") {
217
+ const requestedAmount = only("amount");
218
+ return url.pathname === destination && only("spl-token") === asset.reference && q.getAll("memo").length <= 1 && (paymentDestinationTag === void 0 || only("memo") === paymentDestinationTag) && requestedAmount !== void 0 && /^\d+(?:\.\d+)?$/.test(requestedAmount) && decimal(requestedAmount) === amount;
219
+ }
220
+ return false;
221
+ }
222
+ function payableTokenUri(token, answer) {
223
+ const payment = tokenPayment(token, answer);
224
+ return answer.paymentURI && paysQuote(answer.paymentURI, payment, token.asset) ? answer.paymentURI : payment.destination;
225
+ }
226
+ function normalizeTokenQuote(token, answer, nowMs = Date.now()) {
227
+ const payment = tokenPayment(token, answer);
228
+ const rawExpiry = answer.paymentQuote?.expiresAt;
229
+ const expiresAt = typeof rawExpiry === "string" ? Date.parse(rawExpiry) : NaN;
230
+ if (!Number.isFinite(nowMs) || !Number.isFinite(expiresAt) || expiresAt <= nowMs) {
231
+ throw new LnurlError("The token quote has no valid future expiry");
232
+ }
233
+ return {
234
+ optionId: token.id,
235
+ ...payment,
236
+ value: answer.paymentURI && paysQuote(answer.paymentURI, payment, token.asset) ? answer.paymentURI : payment.destination,
237
+ expiresAt
238
+ };
239
+ }
240
+
241
+ // packages/client/src/payer.ts
242
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
243
+ import { hex as hex2 } from "@scure/base";
244
+
245
+ // packages/client/src/bolt11.ts
246
+ import { bech32 as bech322, hex } from "@scure/base";
247
+ function paymentHashOf(pr) {
248
+ try {
249
+ const { words } = bech322.decode(pr.toLowerCase(), 2e3);
250
+ const end = words.length - 104;
251
+ for (let i = 7; i < end; ) {
252
+ const len = words[i + 1] << 5 | words[i + 2];
253
+ const dataEnd = i + 3 + len;
254
+ if (dataEnd > end) return null;
255
+ if (words[i] === 1 && len === 52) {
256
+ const bytes = bech322.fromWordsUnsafe(words.slice(i + 3, dataEnd));
257
+ return bytes ? hex.encode(bytes.slice(0, 32)) : null;
258
+ }
259
+ i = dataEnd;
260
+ }
261
+ return null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+ var MULTIPLIER_DIVISOR = { m: 1000n, u: 1000000n, n: 1000000000n, p: 1000000000000n };
267
+ function amountMsatOf(pr) {
268
+ let prefix;
269
+ try {
270
+ ({ prefix } = bech322.decode(pr.toLowerCase(), 2e3));
271
+ } catch {
272
+ return void 0;
273
+ }
274
+ const match = /^ln[a-z]+(\d+)([munp])?$/.exec(prefix);
275
+ if (!match) return null;
276
+ const [, digits, mult] = match;
277
+ const divisor = mult ? MULTIPLIER_DIVISOR[mult] : 1n;
278
+ const numerator = BigInt(digits) * 100000000000n;
279
+ if (numerator % divisor !== 0n) return void 0;
280
+ const msat2 = numerator / divisor;
281
+ return msat2 <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(msat2) : void 0;
282
+ }
283
+
284
+ // packages/client/src/payer.ts
285
+ function preimageOpens(preimage, pr) {
286
+ const hash = paymentHashOf(pr);
287
+ if (!hash || !/^[0-9a-f]{64}$/i.test(preimage)) return false;
288
+ return hex2.encode(sha2562(hex2.decode(preimage.toLowerCase()))) === hash;
289
+ }
290
+ function quotableUnit(payRequest, code, amount) {
291
+ const unit = payRequest.units?.find((u) => u.code.toUpperCase() === code.toUpperCase());
292
+ if (!unit) throw new LnurlError(`This payRequest does not quote in ${code}`);
293
+ if (!Number.isSafeInteger(amount) || amount <= 0) throw new LnurlError(`Amount must be a positive whole number of ${unit.code}'s smallest unit`);
294
+ const bound = (b) => b !== void 0 && /^\d+$/.test(b) ? BigInt(b) : void 0;
295
+ const lo = bound(unit.minAmount);
296
+ const hi = bound(unit.maxAmount);
297
+ if (lo !== void 0 && BigInt(amount) < lo || hi !== void 0 && BigInt(amount) > hi) {
298
+ throw new LnurlError(`Amount must be between ${unit.minAmount ?? 1} and ${unit.maxAmount ?? "any"} in ${unit.code}'s smallest unit`);
299
+ }
300
+ return unit;
301
+ }
302
+ function quotedMsat(raw, unit, amount) {
303
+ const q = raw;
304
+ if (q?.requested?.unit !== unit || q.requested.amount !== String(amount)) {
305
+ throw new LnurlError(`The callback did not quote ${amount} ${unit}`);
306
+ }
307
+ const msat2 = q.payment?.unit === "msat" && /^\d+$/.test(q.payment.amount) ? Number(q.payment.amount) : NaN;
308
+ if (!Number.isSafeInteger(msat2) || msat2 <= 0) throw new LnurlError("The callback quoted no millisat amount to pay");
309
+ return msat2;
310
+ }
311
+ async function resolve(input, fetchImpl) {
312
+ const source = toPayRequestUrl(input);
313
+ const body = await lnurlFetch(source.url, void 0, fetchImpl);
314
+ if (body.tag !== "payRequest") throw new LnurlError(`Expected a payRequest, got "${String(body.tag)}"`);
315
+ return { ...body, source };
316
+ }
317
+ async function requestInvoice(payRequest, opts, fetchImpl) {
318
+ if (payRequest.source.surface === "session" && opts.paymentOption !== void 0 && !payRequest.paymentOptions) {
319
+ throw new LnurlError("paymentOption needs a session payRequest that advertises options; this one does not");
320
+ }
321
+ const unit = opts.unit === void 0 ? void 0 : quotableUnit(payRequest, opts.unit, opts.amount);
322
+ const selected = opts.paymentOption ? payRequest.paymentOptions?.find((option) => option.id === opts.paymentOption) : void 0;
323
+ const token = selected?.asset !== void 0 ? tokenOptions(payRequest).find((o) => o.id === selected.id) : void 0;
324
+ if (selected?.asset !== void 0 && !token) throw new LnurlError(`paymentOption ${selected.id} is not a token this client can pay`);
325
+ const min = selected?.minSendable ?? payRequest.minSendable;
326
+ const max = selected?.maxSendable ?? payRequest.maxSendable;
327
+ const amountMsat = unit ? void 0 : opts.amountSat * 1e3;
328
+ if (amountMsat !== void 0 && !Number.isSafeInteger(amountMsat)) {
329
+ throw new LnurlError("Amount must be a whole number of millisats");
330
+ }
331
+ if (amountMsat !== void 0 && (amountMsat < min || amountMsat > max)) {
332
+ throw new LnurlError(`Amount must be between ${min} and ${max} millisats`);
333
+ }
334
+ if (opts.comment !== void 0 && opts.comment.length > 0) {
335
+ const allowed = payRequest.commentAllowed ?? 0;
336
+ if (allowed === 0) throw new LnurlError("This payRequest does not accept comments");
337
+ if (opts.comment.length > allowed) {
338
+ throw new LnurlError(`Comment must be at most ${allowed} characters`);
339
+ }
340
+ }
341
+ const params = new URLSearchParams();
342
+ params.set("amount", String(unit ? opts.amount : amountMsat));
343
+ if (opts.comment) params.set("comment", opts.comment);
344
+ if (opts.paymentOption !== void 0) params.set("paymentOption", opts.paymentOption);
345
+ if (unit) params.set("unit", unit.code);
346
+ const sep = payRequest.callback.includes("?") ? "&" : "?";
347
+ const body = await lnurlFetch(`${payRequest.callback}${sep}${params.toString()}`, void 0, fetchImpl);
348
+ if (typeof body.pr === "string") {
349
+ if (token) throw new LnurlError(`The callback answered token option ${token.id} with an invoice`);
350
+ const invoiceMsat = amountMsatOf(body.pr);
351
+ if (invoiceMsat === void 0) throw new LnurlError("The callback returned an invoice that does not decode");
352
+ const expectedMsat = unit ? quotedMsat(body.paymentQuote, unit.code, opts.amount) : amountMsat;
353
+ if (invoiceMsat !== expectedMsat) {
354
+ throw new LnurlError(`The callback returned an invoice for ${invoiceMsat ?? "no"} millisats, not the ${unit ? "quoted" : "requested"} ${expectedMsat}`);
355
+ }
356
+ const result = { kind: "bolt11", pr: body.pr, verify: typeof body.verify === "string" ? body.verify : void 0 };
357
+ if (typeof body.verifyBatch === "string") result.verifyBatch = body.verifyBatch;
358
+ if (typeof body.paymentOption === "string") result.paymentOption = body.paymentOption;
359
+ if (body.paymentQuote !== void 0) result.paymentQuote = body.paymentQuote;
360
+ return result;
361
+ }
362
+ if (typeof body.paymentOption === "string") {
363
+ const result = {
364
+ kind: "destination",
365
+ paymentOption: body.paymentOption,
366
+ ...typeof body.paymentDestination === "string" ? { paymentDestination: body.paymentDestination } : {},
367
+ ...typeof body.paymentDestinationTag === "string" ? { paymentDestinationTag: body.paymentDestinationTag } : {},
368
+ ...typeof body.paymentURI === "string" ? { paymentURI: body.paymentURI } : {},
369
+ ...body.paymentQuote && typeof body.paymentQuote === "object" ? { paymentQuote: body.paymentQuote } : {},
370
+ ...typeof body.expiresAt === "number" ? { expiresAt: body.expiresAt } : {},
371
+ ...typeof body.provider === "string" ? { provider: body.provider } : {},
372
+ ...typeof body.verify === "string" ? { verify: body.verify } : {},
373
+ ...typeof body.verifyBatch === "string" ? { verifyBatch: body.verifyBatch } : {}
374
+ };
375
+ if (token) {
376
+ const paymentURI = payableTokenUri(token, result);
377
+ if (result.paymentURI !== void 0) result.paymentURI = paymentURI;
378
+ }
379
+ if (unit) quotedMsat(body.paymentQuote, unit.code, opts.amount);
380
+ return result;
381
+ }
382
+ throw new LnurlError("Unexpected callback response");
383
+ }
384
+ function parseVerifyStatus(body) {
385
+ if (typeof body.pr === "string") {
386
+ const settled = body.settled === true;
387
+ const preimage = typeof body.preimage === "string" ? body.preimage : null;
388
+ if (settled && (!preimage || !preimageOpens(preimage, body.pr))) {
389
+ throw new LnurlError("Verify reported settled with a preimage that does not match the invoice's payment hash");
390
+ }
391
+ return { kind: "bolt11", settled, preimage, pr: body.pr };
392
+ }
393
+ if (typeof body.paymentOption !== "string") {
394
+ throw new LnurlTransportError("Verify response is neither a bolt11 nor a destination status");
395
+ }
396
+ return {
397
+ kind: "destination",
398
+ settled: body.settled === true,
399
+ paymentOption: body.paymentOption,
400
+ ...typeof body.paymentDestination === "string" ? { paymentDestination: body.paymentDestination } : {},
401
+ ...typeof body.paymentReference === "string" ? { paymentReference: body.paymentReference } : {}
402
+ };
403
+ }
404
+ function verifyStatusIsFor(status, expected) {
405
+ if ("pr" in expected) {
406
+ const hash = paymentHashOf(expected.pr);
407
+ return status.kind === "bolt11" && hash !== null && paymentHashOf(status.pr) === hash;
408
+ }
409
+ return status.kind === "destination" && status.paymentOption === expected.paymentOption && (status.paymentDestination === void 0 || expected.paymentDestination === void 0 || status.paymentDestination === expected.paymentDestination);
410
+ }
411
+ function sleep(ms, signal) {
412
+ return new Promise((resolve2, reject) => {
413
+ if (signal?.aborted) {
414
+ reject(signal.reason ?? new LnurlError("Aborted"));
415
+ return;
416
+ }
417
+ const timer = setTimeout(() => {
418
+ signal?.removeEventListener("abort", onAbort);
419
+ resolve2();
420
+ }, ms);
421
+ const onAbort = () => {
422
+ clearTimeout(timer);
423
+ reject(signal?.reason ?? new LnurlError("Aborted"));
424
+ };
425
+ signal?.addEventListener("abort", onAbort, { once: true });
426
+ });
427
+ }
428
+ async function pollVerify(verifyUrl, opts, fetchImpl) {
429
+ if (!verifyUrl) throw new LnurlError("verify URL is required");
430
+ const intervalMs = opts?.intervalMs ?? 1e3;
431
+ const timeoutMs = opts?.timeoutMs ?? 12e4;
432
+ const deadline = Date.now() + timeoutMs;
433
+ let lastSnapshot;
434
+ for (; ; ) {
435
+ if (opts?.signal?.aborted) throw opts.signal.reason ?? new LnurlError("Aborted");
436
+ const body = await lnurlFetch(verifyUrl, void 0, fetchImpl);
437
+ const status = parseVerifyStatus(body);
438
+ if (opts?.expect && !verifyStatusIsFor(status, opts.expect)) throw new LnurlError("Verify answered about a different payment");
439
+ lastSnapshot = status;
440
+ opts?.onUpdate?.(status);
441
+ if (status.settled) return status;
442
+ if (Date.now() >= deadline) throw new LnurlTimeoutError(`Verify timed out after ${timeoutMs}ms`, { lastSnapshot });
443
+ await sleep(intervalMs, opts?.signal);
444
+ }
445
+ }
446
+
447
+ // packages/client/src/sse.ts
448
+ function parseFrame(raw) {
449
+ let event = "";
450
+ const dataLines = [];
451
+ for (const line of raw.split("\n")) {
452
+ if (line === "" || line.startsWith(":")) continue;
453
+ const colon = line.indexOf(":");
454
+ if (colon === -1) continue;
455
+ const field = line.slice(0, colon);
456
+ const value = line.slice(colon + 1).startsWith(" ") ? line.slice(colon + 2) : line.slice(colon + 1);
457
+ if (field === "event") event = value;
458
+ else if (field === "data") dataLines.push(value);
459
+ }
460
+ if (dataLines.length === 0) return void 0;
461
+ return { event, data: dataLines.join("\n") };
462
+ }
463
+ function createSseParser() {
464
+ let buffer = "";
465
+ return {
466
+ push(chunk) {
467
+ buffer += chunk.replace(/\r\n|\r/g, "\n");
468
+ const frames = [];
469
+ let idx;
470
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
471
+ const frame = parseFrame(buffer.slice(0, idx));
472
+ buffer = buffer.slice(idx + 2);
473
+ if (frame !== void 0) frames.push(frame);
474
+ }
475
+ if (buffer.length > MAX_FRAME_CHARS) throw new LnurlTransportError(`SSE frame over ${MAX_FRAME_CHARS} characters`);
476
+ return frames;
477
+ }
478
+ };
479
+ }
480
+ var MAX_FRAME_CHARS = 1024 * 1024;
481
+ async function readSseStream(body, onFrame, signal) {
482
+ const parser = createSseParser();
483
+ const reader = body.getReader();
484
+ const decoder = new TextDecoder();
485
+ try {
486
+ for (; ; ) {
487
+ if (signal?.aborted) break;
488
+ const { done, value } = await reader.read();
489
+ if (value) for (const f of parser.push(decoder.decode(value, { stream: !done }))) onFrame(f);
490
+ if (done) break;
491
+ }
492
+ } catch (err) {
493
+ void reader.cancel(err).catch(() => void 0);
494
+ throw err;
495
+ } finally {
496
+ reader.releaseLock();
497
+ }
498
+ }
499
+
500
+ // packages/client/src/verify-batch.ts
501
+ function dedupeUrls(urls) {
502
+ return [...new Set(urls)];
503
+ }
504
+ async function batchVerify(verifyBatchUrl, verifyUrls, opts, fetchImpl) {
505
+ if (!verifyBatchUrl) throw new LnurlError("verifyBatch URL is required");
506
+ const add = dedupeUrls(verifyUrls);
507
+ if (add.length === 0) throw new LnurlError("at least one verify URL is required");
508
+ const f = fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
509
+ return fetchBatch(verifyBatchUrl, add, opts?.signal, f);
510
+ }
511
+ var TOO_LARGE = /* @__PURE__ */ new Set([414, 431]);
512
+ async function fetchBatch(verifyBatchUrl, urls, signal, f) {
513
+ try {
514
+ const body = await lnurlFetch(withVerifyParams(verifyBatchUrl, urls), { signal }, f);
515
+ return parseBatchResults(body, urls);
516
+ } catch (err) {
517
+ if (urls.length < 2 || !(err instanceof LnurlError) || !TOO_LARGE.has(err.httpStatus ?? 0)) throw err;
518
+ const mid = Math.ceil(urls.length / 2);
519
+ const head = await fetchBatch(verifyBatchUrl, urls.slice(0, mid), signal, f);
520
+ const tail = await fetchBatch(verifyBatchUrl, urls.slice(mid), signal, f);
521
+ return { results: Object.assign(/* @__PURE__ */ Object.create(null), head.results, tail.results) };
522
+ }
523
+ }
524
+ function withVerifyParams(verifyBatchUrl, verifyUrls) {
525
+ const params = new URLSearchParams();
526
+ for (const url of verifyUrls) params.append("verify", url);
527
+ return `${verifyBatchUrl}${verifyBatchUrl.includes("?") ? "&" : "?"}${params.toString()}`;
528
+ }
529
+ function parseBatchResults(body, requested) {
530
+ const raw = typeof body.results === "object" && body.results !== null ? body.results : void 0;
531
+ if (!raw) throw new LnurlTransportError("Batch verify response carries no results map");
532
+ const results = /* @__PURE__ */ Object.create(null);
533
+ for (const url of requested) {
534
+ const entry = Object.hasOwn(raw, url) ? raw[url] : void 0;
535
+ if (typeof entry !== "object" || entry === null) {
536
+ results[url] = { kind: "error", reason: "no answer for this verify url" };
537
+ continue;
538
+ }
539
+ const record = entry;
540
+ if (record.status !== "OK") {
541
+ results[url] = { kind: "error", reason: typeof record.reason === "string" ? record.reason : "unknown error" };
542
+ continue;
543
+ }
544
+ try {
545
+ results[url] = { kind: "verify", status: parseVerifyStatus(record) };
546
+ } catch (err) {
547
+ results[url] = { kind: "error", reason: err.message };
548
+ }
549
+ }
550
+ return { results };
551
+ }
552
+ function openVerifyBatchStream(opts, handlers, fetchImpl) {
553
+ if (!opts.verifyBatchUrl) throw new LnurlError("verifyBatch URL is required");
554
+ const openUrls = dedupeUrls(opts.verifyUrls);
555
+ if (openUrls.length === 0) throw new LnurlError("at least one verify URL is required");
556
+ if (opts.signal?.aborted) throw opts.signal.reason ?? new LnurlError("Aborted");
557
+ const f = fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
558
+ const controller = new AbortController();
559
+ const onExternalAbort = () => controller.abort(opts.signal.reason);
560
+ opts.signal?.addEventListener("abort", onExternalAbort, { once: true });
561
+ let sessionId;
562
+ let closed = false;
563
+ function finish() {
564
+ if (closed) return;
565
+ closed = true;
566
+ controller.abort();
567
+ opts.signal?.removeEventListener("abort", onExternalAbort);
568
+ handlers.onClose?.();
569
+ }
570
+ function fail(err) {
571
+ if (closed) return;
572
+ closed = true;
573
+ controller.abort();
574
+ opts.signal?.removeEventListener("abort", onExternalAbort);
575
+ const error = err instanceof LnurlError ? err : err instanceof Error ? new LnurlTransportError(err.message, { cause: err }) : new LnurlTransportError("Batch stream failed");
576
+ handlers.onError?.(error);
577
+ handlers.onClose?.();
578
+ }
579
+ async function applyUpdate(add, remove) {
580
+ const params = new URLSearchParams();
581
+ params.set("session", sessionId);
582
+ for (const url of dedupeUrls(add)) params.append("add", url);
583
+ for (const url of dedupeUrls(remove)) params.append("remove", url);
584
+ const sep = opts.verifyBatchUrl.includes("?") ? "&" : "?";
585
+ try {
586
+ await lnurlFetch(`${opts.verifyBatchUrl}${sep}${params.toString()}`, void 0, f);
587
+ } catch (err) {
588
+ fail(err instanceof LnurlError ? err : new LnurlTransportError("Batch update failed", { cause: err }));
589
+ }
590
+ }
591
+ function handleFrame(event, raw) {
592
+ let body;
593
+ try {
594
+ body = JSON.parse(raw);
595
+ } catch {
596
+ throw new LnurlTransportError("Batch stream carried a frame that is not JSON");
597
+ }
598
+ if (typeof body !== "object" || body === null) throw new LnurlTransportError("Batch stream carried an empty frame");
599
+ const record = body;
600
+ if (event === "session") {
601
+ if (typeof record.session === "string") sessionId = record.session;
602
+ return;
603
+ }
604
+ if (event === "removed") {
605
+ if (typeof record.verify === "string") handlers.onRemoved?.(record.verify);
606
+ return;
607
+ }
608
+ const url = typeof record.verify === "string" ? record.verify : void 0;
609
+ if (!url) throw new LnurlTransportError("Batch stream carried a result frame without a verify url");
610
+ handlers.onUpdate?.(url, parseVerifyStatus(record));
611
+ }
612
+ void (async () => {
613
+ try {
614
+ const openUrl = withVerifyParams(opts.verifyBatchUrl, openUrls);
615
+ let response = await f(openUrl, { headers: { Accept: "text/event-stream" }, signal: controller.signal });
616
+ if (response.status === 406) response = await f(openUrl, { signal: controller.signal });
617
+ if (!response.ok) throw new LnurlError(response.statusText, { httpStatus: response.status });
618
+ const contentType = response.headers.get("content-type") ?? "";
619
+ if (!contentType.includes("text/event-stream")) {
620
+ const snapshot = await response.json();
621
+ const parsed = parseBatchResults(snapshot, openUrls);
622
+ for (const [url, item] of Object.entries(parsed.results)) {
623
+ if (item.kind === "verify") handlers.onUpdate?.(url, item.status);
624
+ }
625
+ finish();
626
+ return;
627
+ }
628
+ await readSseStream(response.body, (frame) => {
629
+ handleFrame(frame.event, frame.data);
630
+ }, controller.signal);
631
+ } catch (err) {
632
+ if (opts.signal?.aborted) {
633
+ finish();
634
+ return;
635
+ }
636
+ fail(err);
637
+ return;
638
+ }
639
+ finish();
640
+ })();
641
+ return {
642
+ get sessionId() {
643
+ return sessionId;
644
+ },
645
+ update(add, remove = []) {
646
+ if (closed) throw new LnurlError("Batch stream has ended");
647
+ if (!sessionId) throw new LnurlError("Batch stream has not yet announced its session");
648
+ void applyUpdate(add, remove);
649
+ },
650
+ close() {
651
+ finish();
652
+ }
653
+ };
654
+ }
655
+
656
+ // packages/client/src/session.ts
657
+ function postJson(url, token, body, fetchImpl) {
658
+ return apiFetch(url, {
659
+ method: "POST",
660
+ headers: { "content-type": "application/json", Authorization: `Bearer ${token}` },
661
+ body: JSON.stringify(body)
662
+ }, fetchImpl).then(() => void 0);
663
+ }
664
+ async function openSession(baseUrl, opts, handlers, fetchImpl) {
665
+ const root = baseUrl.replace(/\/+$/, "");
666
+ const token = opts.token;
667
+ const reconnect = opts.reconnect;
668
+ const maxAttempts = reconnect === false ? 0 : reconnect?.maxAttempts ?? 5;
669
+ const baseDelayMs = (typeof reconnect === "object" ? reconnect.baseDelayMs : void 0) ?? 500;
670
+ const resumable = token !== void 0 && reconnect !== false;
671
+ const signal = opts.signal;
672
+ const backoffMs = (attempt) => Math.min(baseDelayMs * 2 ** (attempt - 1), 3e4);
673
+ let closed = false;
674
+ let timer;
675
+ let activeBody = null;
676
+ let info = null;
677
+ let streamDeliveredFrame = false;
678
+ let resolveOpened;
679
+ let rejectOpened;
680
+ const opened = new Promise((res, rej) => {
681
+ resolveOpened = res;
682
+ rejectOpened = rej;
683
+ });
684
+ const close = () => {
685
+ if (closed) return;
686
+ closed = true;
687
+ if (timer !== void 0) {
688
+ clearTimeout(timer);
689
+ timer = void 0;
690
+ }
691
+ const body = activeBody;
692
+ activeBody = null;
693
+ if (body) void body.cancel().catch(() => void 0);
694
+ };
695
+ const makeSession = (created) => ({
696
+ sessionId: created.sessionId,
697
+ lnurl: created.lnurl,
698
+ token: created.token,
699
+ get closed() {
700
+ return closed;
701
+ },
702
+ reportSettled: (preimage) => postJson(`${root}/lnurl/session/${created.sessionId}/settled`, created.token, { preimage }, fetchImpl),
703
+ close
704
+ });
705
+ const postSession = async () => {
706
+ let response;
707
+ try {
708
+ response = await fetchImpl(`${root}/lnurl/session`, {
709
+ method: "POST",
710
+ headers: { "content-type": "application/json" },
711
+ body: JSON.stringify(token !== void 0 ? { token } : {}),
712
+ signal
713
+ });
714
+ } catch (err) {
715
+ if (err instanceof LnurlError) throw err;
716
+ throw new LnurlTransportError("Session request failed", { cause: err });
717
+ }
718
+ if (!response.ok) {
719
+ let reason = response.statusText;
720
+ let code;
721
+ try {
722
+ const data = await response.json();
723
+ if (typeof data.error === "string") reason = data.error;
724
+ else if (typeof data.reason === "string") reason = data.reason;
725
+ if (typeof data.code === "string") code = data.code;
726
+ } catch {
727
+ }
728
+ throw new LnurlError(reason, { httpStatus: response.status, code });
729
+ }
730
+ if (response.body === null) throw new LnurlTransportError("Session stream has no readable body");
731
+ return response;
732
+ };
733
+ const onFrame = (frame) => {
734
+ streamDeliveredFrame = true;
735
+ if (frame.event === "session_created") {
736
+ if (info) return;
737
+ let created;
738
+ try {
739
+ created = JSON.parse(frame.data);
740
+ } catch (err) {
741
+ rejectOpened(new LnurlTransportError("Invalid session_created frame", { cause: err }));
742
+ return;
743
+ }
744
+ const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
745
+ if (!isNonEmptyString(created.sessionId) || !isNonEmptyString(created.lnurl) || !isNonEmptyString(created.token)) {
746
+ rejectOpened(new LnurlTransportError("session_created frame is missing sessionId, lnurl or token"));
747
+ return;
748
+ }
749
+ info = created;
750
+ resolveOpened(makeSession(created));
751
+ return;
752
+ }
753
+ const current = info;
754
+ if (!current) return;
755
+ if (frame.event === "invoice_request") {
756
+ let req;
757
+ try {
758
+ req = JSON.parse(frame.data);
759
+ } catch (err) {
760
+ handlers.onError?.(new LnurlTransportError("Invalid invoice_request frame", { cause: err }));
761
+ return;
762
+ }
763
+ const respond = {
764
+ answerInvoice: (pr) => postJson(`${root}/lnurl/session/${current.sessionId}/invoice`, current.token, { pr }, fetchImpl),
765
+ rejectInvoice: (reason) => postJson(`${root}/lnurl/session/${current.sessionId}/invoice`, current.token, { error: reason }, fetchImpl)
766
+ };
767
+ try {
768
+ const result = handlers.onInvoiceRequest(
769
+ req.comment === void 0 ? { amountMsat: req.amountMsat } : { amountMsat: req.amountMsat, comment: req.comment },
770
+ respond
771
+ );
772
+ if (result instanceof Promise) result.catch((err) => handlers.onError?.(err));
773
+ } catch (err) {
774
+ handlers.onError?.(err);
775
+ }
776
+ return;
777
+ }
778
+ if (frame.event === "invoice_settled") {
779
+ try {
780
+ handlers.onSettled?.(JSON.parse(frame.data));
781
+ } catch (err) {
782
+ handlers.onError?.(new LnurlTransportError("Invalid invoice_settled frame", { cause: err }));
783
+ }
784
+ return;
785
+ }
786
+ if (frame.event === "error") {
787
+ let message = frame.data;
788
+ try {
789
+ const data = JSON.parse(frame.data);
790
+ if (typeof data.error === "string") message = data.error;
791
+ } catch {
792
+ }
793
+ handlers.onError?.(new Error(message));
794
+ }
795
+ };
796
+ const sleep2 = (ms) => new Promise((resolve2) => {
797
+ timer = setTimeout(() => {
798
+ timer = void 0;
799
+ resolve2();
800
+ }, ms);
801
+ });
802
+ const run = async () => {
803
+ let attempts = 0;
804
+ for (; ; ) {
805
+ streamDeliveredFrame = false;
806
+ let response;
807
+ try {
808
+ response = await postSession();
809
+ } catch (err) {
810
+ if (!info) {
811
+ rejectOpened(err);
812
+ return;
813
+ }
814
+ if (err instanceof LnurlError && err.httpStatus === 409) {
815
+ handlers.onError?.(err);
816
+ return;
817
+ }
818
+ if (closed || signal?.aborted || !resumable || attempts >= maxAttempts) {
819
+ handlers.onError?.(err);
820
+ return;
821
+ }
822
+ attempts += 1;
823
+ handlers.onReconnect?.(attempts);
824
+ await sleep2(backoffMs(attempts));
825
+ continue;
826
+ }
827
+ const body = response.body;
828
+ activeBody = body;
829
+ try {
830
+ await readSseStream(body, onFrame, signal);
831
+ } catch (err) {
832
+ if (closed || signal?.aborted) return;
833
+ if (!info) {
834
+ rejectOpened(err);
835
+ return;
836
+ }
837
+ handlers.onError?.(err);
838
+ } finally {
839
+ if (activeBody === body) activeBody = null;
840
+ }
841
+ if (!info) {
842
+ rejectOpened(new LnurlTransportError("Session stream ended before session_created"));
843
+ return;
844
+ }
845
+ if (streamDeliveredFrame) attempts = 0;
846
+ if (closed || signal?.aborted || !resumable || attempts >= maxAttempts) return;
847
+ attempts += 1;
848
+ handlers.onReconnect?.(attempts);
849
+ await sleep2(backoffMs(attempts));
850
+ }
851
+ };
852
+ void run();
853
+ return opened;
854
+ }
855
+
856
+ // packages/client/src/token.ts
857
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
858
+ import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
859
+ import { hex as hex3 } from "@scure/base";
860
+ var MESSAGE_PREFIX = "lnurl-session:";
861
+ function sessionTokenMessage(domain) {
862
+ const normalised = domain.trim().toLowerCase();
863
+ if (!normalised) throw new Error("a session token must be bound to a domain");
864
+ return sha2563(new TextEncoder().encode(MESSAGE_PREFIX + normalised));
865
+ }
866
+ function tokenFromSignature(signature) {
867
+ return hex3.encode(sha2563(signature));
868
+ }
869
+ function deriveSessionToken(privateKeyHex, domain) {
870
+ const signature = secp256k1.sign(sessionTokenMessage(domain), hex3.decode(privateKeyHex), { prehash: false });
871
+ return tokenFromSignature(signature);
872
+ }
873
+ async function deriveSessionTokenWithSigner(signMessage, domain) {
874
+ const message = sessionTokenMessage(domain);
875
+ const first = await signMessage(message, "ecdsa");
876
+ const second = await signMessage(message, "ecdsa");
877
+ if (first.length !== second.length || !first.every((b, i) => b === second[i])) {
878
+ throw new Error(
879
+ "signer is not deterministic: signing the same message twice differed, so the derived token would change on every call and orphan the address. Use RFC6979 ECDSA, not randomised schnorr."
880
+ );
881
+ }
882
+ return tokenFromSignature(first);
883
+ }
884
+ function deriveSessionId(tokenHex) {
885
+ return hex3.encode(sha2563(hex3.decode(tokenHex))).slice(0, 32);
886
+ }
887
+
888
+ // packages/client/src/addresses.ts
889
+ function rootOf(baseUrl) {
890
+ return baseUrl.replace(/\/+$/, "");
891
+ }
892
+ var asRegistered = (a) => ({
893
+ ...a,
894
+ handle: a.handle ?? a.username,
895
+ nameless: a.nameless ?? false,
896
+ sessionLnurl: a.sessionLnurl ?? null
897
+ });
898
+ var asListed = (a) => ({
899
+ ...a,
900
+ handle: a.handle ?? a.username,
901
+ nameless: a.nameless ?? false,
902
+ sessionLnurl: a.sessionLnurl ?? null
903
+ });
904
+ function registerAddress(baseUrl, req, fetchImpl) {
905
+ const headers = { "content-type": "application/json" };
906
+ if (req.apiKey !== void 0) headers["X-API-Key"] = req.apiKey;
907
+ const body = { token: req.token };
908
+ if (req.username !== void 0) body["username"] = req.username;
909
+ if (req.claimCode !== void 0) body["claimCode"] = req.claimCode;
910
+ if (req.domain !== void 0) body["domain"] = req.domain;
911
+ if (req.nameless !== void 0) body["nameless"] = req.nameless;
912
+ return apiFetch(`${rootOf(baseUrl)}/lnurl/address`, {
913
+ method: "POST",
914
+ headers,
915
+ body: JSON.stringify(body)
916
+ }, fetchImpl).then(asRegistered);
917
+ }
918
+ function listAddresses(baseUrl, token, fetchImpl) {
919
+ return apiFetch(`${rootOf(baseUrl)}/lnurl/address`, {
920
+ headers: { Authorization: `Bearer ${token}` }
921
+ }, fetchImpl).then((entries) => entries.map(asListed));
922
+ }
923
+ function upgradeAddress(baseUrl, req, fetchImpl) {
924
+ const body = {};
925
+ if (req.username !== void 0) body["username"] = req.username;
926
+ if (req.claimCode !== void 0) body["claimCode"] = req.claimCode;
927
+ const query = req.domain !== void 0 ? `?domain=${encodeURIComponent(req.domain)}` : "";
928
+ return apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(req.handle)}${query}`, {
929
+ method: "PATCH",
930
+ headers: { "content-type": "application/json", Authorization: `Bearer ${req.token}` },
931
+ body: JSON.stringify(body)
932
+ }, fetchImpl).then(asRegistered);
933
+ }
934
+ function domainCapabilities(baseUrl, opts, fetchImpl) {
935
+ const query = opts?.domain !== void 0 ? `?domain=${encodeURIComponent(opts.domain)}` : "";
936
+ return apiFetch(`${rootOf(baseUrl)}/lnurl/domain${query}`, void 0, fetchImpl);
937
+ }
938
+ function toActivity(row) {
939
+ if (row.paymentOption === "lightning") {
940
+ const activity2 = {
941
+ kind: "bolt11",
942
+ paymentHash: row.paymentHash,
943
+ pr: row.pr,
944
+ preimage: row.preimage,
945
+ swapId: row.swapId,
946
+ payoutReference: row.payoutReference ?? null,
947
+ settled: row.settled,
948
+ amountMsat: row.amountMsat,
949
+ createdAt: row.createdAt,
950
+ settledAt: row.settledAt,
951
+ ...row.quote ? { quote: row.quote } : {}
952
+ };
953
+ return activity2;
954
+ }
955
+ const activity = {
956
+ kind: "destination",
957
+ verifyId: row.paymentHash,
958
+ paymentOption: row.paymentOption,
959
+ paymentDestination: row.paymentDestination,
960
+ covenantScript: row.covenantScript,
961
+ paymentReference: row.paymentReference,
962
+ payoutReference: row.payoutReference ?? null,
963
+ settled: row.settled,
964
+ amountMsat: row.amountMsat,
965
+ createdAt: row.createdAt,
966
+ settledAt: row.settledAt,
967
+ ...row.quote ? { quote: row.quote } : {}
968
+ };
969
+ return activity;
970
+ }
971
+ async function listPayments(baseUrl, token, handle, opts, fetchImpl) {
972
+ const params = new URLSearchParams();
973
+ if (opts?.domain !== void 0) params.set("domain", opts.domain);
974
+ if (opts?.since !== void 0) params.set("since", String(opts.since));
975
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
976
+ const query = params.size > 0 ? `?${params.toString()}` : "";
977
+ const page = await apiFetch(
978
+ `${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(handle)}/payments${query}`,
979
+ {
980
+ headers: { Authorization: `Bearer ${token}` }
981
+ },
982
+ fetchImpl
983
+ );
984
+ return {
985
+ source: { ...page.source, handle: page.source.handle ?? handle },
986
+ payments: page.payments.map(toActivity),
987
+ nextSince: page.nextSince
988
+ };
989
+ }
990
+ async function revokeAddress(baseUrl, token, handle, opts, fetchImpl) {
991
+ const query = opts?.domain !== void 0 ? `?domain=${encodeURIComponent(opts.domain)}` : "";
992
+ await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(handle)}${query}`, {
993
+ method: "DELETE",
994
+ headers: { Authorization: `Bearer ${token}` }
995
+ }, fetchImpl);
996
+ }
997
+ var COMPRESSED_KEY = /^0[23][0-9a-f]{64}$/i;
998
+ async function registerArkadeIdentity(baseUrl, req, fetchImpl) {
999
+ if (!req.hd && !COMPRESSED_KEY.test(req.claimPublicKey)) {
1000
+ throw new LnurlError("claimPublicKey must be a compressed 33-byte public key (02/03 prefix plus 64 hex chars)");
1001
+ }
1002
+ if (req.hd && (req.arkadeAddress !== void 0 || req.claimPublicKey !== void 0 || req.boardingAddress !== void 0)) {
1003
+ throw new LnurlError("HD receive cannot include a static wallet destination");
1004
+ }
1005
+ const body = req.hd ? { hd: req.hd } : { arkadeAddress: req.arkadeAddress, claimPublicKey: req.claimPublicKey };
1006
+ if (!req.hd && req.boardingAddress !== void 0) body["boardingAddress"] = req.boardingAddress;
1007
+ if (req.domain !== void 0) body["domain"] = req.domain;
1008
+ const result = await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(req.handle)}/arkade`, {
1009
+ method: "POST",
1010
+ headers: { "content-type": "application/json", Authorization: `Bearer ${req.token}` },
1011
+ body: JSON.stringify(body)
1012
+ }, fetchImpl);
1013
+ if (req.hd && result?.receiveMode !== "hd") throw new LnurlError("Server did not confirm HD receive mode");
1014
+ }
1015
+ async function listHdAllocations(baseUrl, token, handle, opts, fetchImpl) {
1016
+ if (opts?.after !== void 0 && (!Number.isSafeInteger(opts.after) || opts.after < 0)) {
1017
+ throw new LnurlError("Allocation cursor must be a non-negative integer");
1018
+ }
1019
+ if (opts?.limit !== void 0 && (!Number.isSafeInteger(opts.limit) || opts.limit <= 0)) {
1020
+ throw new LnurlError("Allocation limit must be a positive integer");
1021
+ }
1022
+ const params = new URLSearchParams();
1023
+ if (opts?.domain !== void 0) params.set("domain", opts.domain);
1024
+ if (opts?.after !== void 0) params.set("after", String(opts.after));
1025
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
1026
+ const query = params.size ? `?${params.toString()}` : "";
1027
+ const page = await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(handle)}/arkade/allocations${query}`, {
1028
+ headers: { Authorization: `Bearer ${token}` }
1029
+ }, fetchImpl);
1030
+ if (!page || !Array.isArray(page.allocations) || !Number.isSafeInteger(page.nextAfter)) {
1031
+ throw new LnurlError("Invalid HD allocation page");
1032
+ }
1033
+ let previous = opts?.after ?? 0;
1034
+ for (const allocation of page.allocations) {
1035
+ if (!allocation || !Number.isSafeInteger(allocation.id) || allocation.id <= previous || !Number.isSafeInteger(allocation.index) || allocation.index < 0 || allocation.index >= 2147483648 || typeof allocation.descriptor !== "string" || typeof allocation.arkadeAddress !== "string" || !COMPRESSED_KEY.test(allocation.claimPublicKey) || !allocation.hd || allocation.hd.version !== 1 || !Number.isSafeInteger(allocation.createdAt)) throw new LnurlError("Invalid HD allocation page");
1036
+ previous = allocation.id;
1037
+ }
1038
+ if (page.nextAfter !== previous) throw new LnurlError("Invalid HD allocation cursor");
1039
+ return page;
1040
+ }
1041
+
1042
+ // packages/client/src/sync.ts
1043
+ var DEFAULT_SYNC_PAGE_LIMIT = 50;
1044
+ var SYNC_MAX_ATTEMPTS = 3;
1045
+ var SYNC_RETRY_DELAY_MS = 250;
1046
+ var NEVER_SETTLES = /* @__PURE__ */ new Set(["onchain"]);
1047
+ var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1048
+ var isRetryable = (error) => {
1049
+ if (error instanceof LnurlError) return error.retryable;
1050
+ return error?.retryable === true;
1051
+ };
1052
+ var toStored = (baseUrl, domain, lightningAddress, handle, entry) => {
1053
+ const identifier = entry.kind === "bolt11" ? entry.paymentHash : entry.verifyId;
1054
+ return {
1055
+ key: `${baseUrl}|${identifier}`,
1056
+ baseUrl,
1057
+ domain,
1058
+ lightningAddress,
1059
+ handle,
1060
+ identifier,
1061
+ kind: entry.kind,
1062
+ settled: entry.settled,
1063
+ amountMsat: entry.amountMsat,
1064
+ createdAt: entry.createdAt,
1065
+ settledAt: entry.settledAt,
1066
+ swapId: entry.kind === "bolt11" ? entry.swapId : null,
1067
+ paymentReference: entry.kind === "destination" ? entry.paymentReference : null,
1068
+ // Present on both shapes: every rail eventually credits the owner somewhere.
1069
+ payoutReference: entry.payoutReference ?? null,
1070
+ preimage: entry.kind === "bolt11" ? entry.preimage : null,
1071
+ paymentOption: entry.kind === "destination" ? entry.paymentOption : null,
1072
+ covenantScript: entry.kind === "destination" ? entry.covenantScript : null,
1073
+ ...entry.quote ? { quote: entry.quote } : {}
1074
+ };
1075
+ };
1076
+ var mayStillSettle = (row) => !row.settled && (row.paymentOption === null || !NEVER_SETTLES.has(row.paymentOption));
1077
+ var resumeFrom = (tail, fallback) => {
1078
+ let oldest = fallback;
1079
+ for (const row of tail) if (mayStillSettle(row) && row.createdAt < oldest) oldest = row.createdAt;
1080
+ return oldest;
1081
+ };
1082
+ var listWithBackoff = (client, token, handle, domain, since, limit) => {
1083
+ const attempt = async (tries) => {
1084
+ try {
1085
+ return await client.listPayments(token, handle, { domain, since, limit });
1086
+ } catch (error) {
1087
+ if (!isRetryable(error) || tries >= SYNC_MAX_ATTEMPTS) throw error;
1088
+ await wait(tries * SYNC_RETRY_DELAY_MS);
1089
+ return attempt(tries + 1);
1090
+ }
1091
+ };
1092
+ return attempt(1);
1093
+ };
1094
+ var syncTarget = async (target, client, store, limit, onStored) => {
1095
+ const watermarkKey = `${target.handle}@${target.domain}`;
1096
+ let since = await store.readWatermark(target.baseUrl, watermarkKey);
1097
+ const tail = [];
1098
+ for (; ; ) {
1099
+ const page = await listWithBackoff(client, target.token, target.handle, target.domain, since, limit);
1100
+ const records = page.payments.map(
1101
+ (entry) => toStored(target.baseUrl, page.source.domain, page.source.lightningAddress, page.source.handle, entry)
1102
+ );
1103
+ await store.upsert(records);
1104
+ onStored(records.length);
1105
+ for (const record of records) tail.push(record);
1106
+ if (tail.length > limit) tail.splice(0, tail.length - limit);
1107
+ const previous = since;
1108
+ since = page.nextSince;
1109
+ await store.writeWatermark(target.baseUrl, watermarkKey, resumeFrom(tail, since));
1110
+ if (page.payments.length < limit) return;
1111
+ if (previous !== void 0 && since <= previous) {
1112
+ throw new LnurlError(
1113
+ `payment sync stalled for ${watermarkKey} at ${target.baseUrl}: ${limit} payments share timestamp ${since}, so the cursor cannot advance past them`
1114
+ );
1115
+ }
1116
+ }
1117
+ };
1118
+ function syncPayments(targets, opts) {
1119
+ const run = async () => {
1120
+ const limit = opts.limit ?? DEFAULT_SYNC_PAGE_LIMIT;
1121
+ let synced = 0;
1122
+ const failures = [];
1123
+ for (const target of targets) {
1124
+ try {
1125
+ await syncTarget(target, opts.client(target.baseUrl), opts.store, limit, (count) => {
1126
+ synced += count;
1127
+ });
1128
+ } catch (error) {
1129
+ failures.push({ baseUrl: target.baseUrl, error });
1130
+ }
1131
+ }
1132
+ return { synced, failures };
1133
+ };
1134
+ return run();
1135
+ }
1136
+
1137
+ // packages/client/src/stores.ts
1138
+ var RECORDS = "arkade-lnurl.payments";
1139
+ var WATERMARKS = "arkade-lnurl.watermarks";
1140
+ var area = () => {
1141
+ if (typeof localStorage === "undefined") {
1142
+ throw new LnurlError("browserPaymentStore needs localStorage; supply your own PaymentSyncStore");
1143
+ }
1144
+ return localStorage;
1145
+ };
1146
+ var read = (key, fallback) => {
1147
+ try {
1148
+ const raw = area().getItem(key);
1149
+ return raw ? JSON.parse(raw) : fallback;
1150
+ } catch {
1151
+ return fallback;
1152
+ }
1153
+ };
1154
+ function browserPaymentStore(prefix = "") {
1155
+ const records = `${prefix}${RECORDS}`;
1156
+ const marks = `${prefix}${WATERMARKS}`;
1157
+ return {
1158
+ async upsert(incoming) {
1159
+ const byKey = new Map(read(records, []).map((r) => [r.key, r]));
1160
+ for (const record of incoming) byKey.set(record.key, record);
1161
+ area().setItem(records, JSON.stringify([...byKey.values()]));
1162
+ },
1163
+ async readWatermark(baseUrl, lightningAddress) {
1164
+ return read(marks, {})[`${baseUrl}|${lightningAddress}`];
1165
+ },
1166
+ async writeWatermark(baseUrl, lightningAddress, since) {
1167
+ const all = read(marks, {});
1168
+ all[`${baseUrl}|${lightningAddress}`] = since;
1169
+ area().setItem(marks, JSON.stringify(all));
1170
+ }
1171
+ };
1172
+ }
1173
+ function storedPayments(address, prefix = "") {
1174
+ const mine = typeof address === "string" ? (r) => r.lightningAddress === address : (r) => r.domain === address.domain && (r.handle === address.handle || r.handle === void 0 && r.lightningAddress === `${address.handle}@${address.domain}`);
1175
+ return read(`${prefix}${RECORDS}`, []).filter(mine).sort((a, b) => b.createdAt - a.createdAt);
1176
+ }
1177
+ function forgetStoredPayments(prefix = "") {
1178
+ area().removeItem(`${prefix}${RECORDS}`);
1179
+ area().removeItem(`${prefix}${WATERMARKS}`);
1180
+ }
1181
+
1182
+ // packages/client/src/confirmations.ts
1183
+ var POLL_MS = 2e3;
1184
+ var BATCH_TIMEOUT_MS = 1e4;
1185
+ var CONFIRMATION_TIMEOUT_MS = 18e4;
1186
+ function createPendingConfirmations(client = createLnurlClient()) {
1187
+ const entries = /* @__PURE__ */ new Set();
1188
+ const batches = /* @__PURE__ */ new Set();
1189
+ let timer;
1190
+ let ticking = false;
1191
+ let generation = 0;
1192
+ function maybeStop() {
1193
+ if (timer && entries.size === 0) {
1194
+ clearInterval(timer);
1195
+ timer = void 0;
1196
+ }
1197
+ }
1198
+ function finish(entry, notify) {
1199
+ if (!entries.delete(entry)) return;
1200
+ entry.soloPolling?.abort();
1201
+ maybeStop();
1202
+ try {
1203
+ notify();
1204
+ } catch {
1205
+ }
1206
+ }
1207
+ const fail = (entry, err) => finish(entry, () => entry.onError(err));
1208
+ const expire = (entry) => {
1209
+ if (Date.now() < entry.deadline) return false;
1210
+ fail(entry, new Error("receiver did not confirm settlement in time"));
1211
+ return true;
1212
+ };
1213
+ function settle(entry, status) {
1214
+ if (expire(entry)) return;
1215
+ if (entry.expect && !verifyStatusIsFor(status, entry.expect)) {
1216
+ fail(entry, new Error("verify answered about a different payment"));
1217
+ } else if (status.settled) {
1218
+ finish(entry, () => entry.onSettled(status));
1219
+ }
1220
+ }
1221
+ async function tick(current) {
1222
+ const now = Date.now();
1223
+ for (const entry of [...entries]) expire(entry);
1224
+ const groups = /* @__PURE__ */ new Map();
1225
+ for (const entry of entries) {
1226
+ if (entry.soloPolling) continue;
1227
+ if (entry.verifyBatch === void 0) {
1228
+ entry.soloPolling = new AbortController();
1229
+ void Promise.resolve().then(() => client.pollVerify(entry.verifyUrl, {
1230
+ intervalMs: POLL_MS,
1231
+ timeoutMs: entry.deadline - now,
1232
+ signal: entry.soloPolling.signal,
1233
+ ...entry.expect ? { expect: entry.expect } : {}
1234
+ })).then((status) => settle(entry, status)).catch((err) => fail(entry, err instanceof Error ? err : new Error(String(err))));
1235
+ } else {
1236
+ const group = groups.get(entry.verifyBatch) ?? [];
1237
+ group.push(entry);
1238
+ groups.set(entry.verifyBatch, group);
1239
+ }
1240
+ }
1241
+ for (const [endpoint, group] of groups) {
1242
+ if (current !== generation) return;
1243
+ const active = group.filter((entry) => entries.has(entry) && !expire(entry));
1244
+ if (!active.length) continue;
1245
+ const abort = new AbortController();
1246
+ batches.add(abort);
1247
+ const cancelled = new Promise((_, reject) => {
1248
+ abort.signal.addEventListener("abort", () => reject(new Error("batch verify aborted")), { once: true });
1249
+ });
1250
+ const cutoff = setTimeout(() => abort.abort(), Math.max(
1251
+ 0,
1252
+ Math.min(BATCH_TIMEOUT_MS, ...active.map((entry) => entry.deadline - Date.now()))
1253
+ ));
1254
+ try {
1255
+ const body = await Promise.race([
1256
+ client.batchVerify(endpoint, [...new Set(active.map((entry) => entry.verifyUrl))], { signal: abort.signal }),
1257
+ cancelled
1258
+ ]);
1259
+ for (const entry of active) {
1260
+ const answer = body.results[entry.verifyUrl];
1261
+ if (!answer || answer.kind !== "verify") {
1262
+ fail(entry, new Error(answer?.reason ?? "no answer for this verify URL"));
1263
+ } else {
1264
+ settle(entry, answer.status);
1265
+ }
1266
+ }
1267
+ } catch {
1268
+ } finally {
1269
+ clearTimeout(cutoff);
1270
+ batches.delete(abort);
1271
+ }
1272
+ }
1273
+ }
1274
+ function ensure() {
1275
+ if (timer) return;
1276
+ timer = setInterval(() => {
1277
+ if (ticking) return;
1278
+ const current = generation;
1279
+ ticking = true;
1280
+ void tick(current).finally(() => {
1281
+ if (current === generation) ticking = false;
1282
+ });
1283
+ }, POLL_MS);
1284
+ }
1285
+ return {
1286
+ add(confirmation) {
1287
+ if ([...entries].some((entry) => (entry.key ?? entry.verifyUrl) === (confirmation.key ?? confirmation.verifyUrl))) return;
1288
+ entries.add({
1289
+ ...confirmation,
1290
+ deadline: (confirmation.startedAt ?? Date.now()) + (confirmation.timeoutMs ?? CONFIRMATION_TIMEOUT_MS)
1291
+ });
1292
+ ensure();
1293
+ },
1294
+ forget() {
1295
+ const forgotten = [...entries];
1296
+ entries.clear();
1297
+ generation++;
1298
+ ticking = false;
1299
+ maybeStop();
1300
+ for (const entry of forgotten) entry.soloPolling?.abort();
1301
+ for (const abort of batches) abort.abort();
1302
+ batches.clear();
1303
+ }
1304
+ };
1305
+ }
1306
+
1307
+ // packages/client/src/repository.ts
1308
+ function createLnurlPaymentRepository(payments, watermarks) {
1309
+ const inFlight = /* @__PURE__ */ new Set();
1310
+ let closed = false;
1311
+ const write = async (work) => {
1312
+ if (closed) return;
1313
+ const pending = Promise.resolve().then(work);
1314
+ inFlight.add(pending);
1315
+ try {
1316
+ await pending;
1317
+ } finally {
1318
+ inFlight.delete(pending);
1319
+ }
1320
+ };
1321
+ return {
1322
+ all: () => payments.read(),
1323
+ upsert: (records) => write(() => payments.write(records)),
1324
+ readWatermark: (baseUrl, address) => watermarks.read(baseUrl, address),
1325
+ writeWatermark: (baseUrl, address, since) => write(() => watermarks.write(baseUrl, address, since)),
1326
+ clear: async () => {
1327
+ await watermarks.clear();
1328
+ await payments.clear();
1329
+ },
1330
+ closeWrites: () => {
1331
+ closed = true;
1332
+ },
1333
+ reopenWrites: () => {
1334
+ closed = false;
1335
+ },
1336
+ writesClosed: () => closed,
1337
+ writesSettled: async () => {
1338
+ await Promise.allSettled([...inFlight]);
1339
+ }
1340
+ };
1341
+ }
1342
+ async function clearLnurlPaymentsForReset(repository) {
1343
+ repository.closeWrites();
1344
+ try {
1345
+ await repository.writesSettled();
1346
+ await repository.clear();
1347
+ } catch (error) {
1348
+ repository.reopenWrites();
1349
+ throw error;
1350
+ }
1351
+ }
1352
+
1353
+ // packages/client/src/index.ts
1354
+ function createLnurlClient(opts) {
1355
+ const baseUrl = opts?.baseUrl;
1356
+ const fetchImpl = opts?.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
1357
+ const needBase = (method) => {
1358
+ if (!baseUrl) throw new LnurlError(`baseUrl is required for ${method}`);
1359
+ return baseUrl;
1360
+ };
1361
+ return {
1362
+ resolve: (input) => resolve(input, fetchImpl),
1363
+ requestInvoice: (payRequest, reqOpts) => requestInvoice(payRequest, reqOpts, fetchImpl),
1364
+ pollVerify: (verifyUrl, pollOpts) => pollVerify(verifyUrl, pollOpts, fetchImpl),
1365
+ batchVerify: (verifyBatchUrl, verifyUrls, opts2) => batchVerify(verifyBatchUrl, verifyUrls, opts2, opts2?.fetchImpl ?? fetchImpl),
1366
+ openSession: async (sessionOpts, handlers) => openSession(needBase("openSession"), sessionOpts, handlers, fetchImpl),
1367
+ openVerifyBatchStream: (streamOpts, handlers) => openVerifyBatchStream(streamOpts, handlers, streamOpts?.fetchImpl),
1368
+ registerAddress: async (req) => registerAddress(needBase("registerAddress"), req, fetchImpl),
1369
+ upgradeAddress: async (req) => upgradeAddress(needBase("upgradeAddress"), req, fetchImpl),
1370
+ domainCapabilities: async (capOpts) => domainCapabilities(needBase("domainCapabilities"), capOpts, fetchImpl),
1371
+ listAddresses: async (token) => listAddresses(needBase("listAddresses"), token, fetchImpl),
1372
+ revokeAddress: async (token, handle, revokeOpts) => revokeAddress(needBase("revokeAddress"), token, handle, revokeOpts, fetchImpl),
1373
+ registerArkadeIdentity: async (req) => registerArkadeIdentity(needBase("registerArkadeIdentity"), req, fetchImpl),
1374
+ listHdAllocations: async (token, handle, listOpts) => listHdAllocations(needBase("listHdAllocations"), token, handle, listOpts, fetchImpl),
1375
+ listPayments: async (token, handle, listOpts) => listPayments(needBase("listPayments"), token, handle, listOpts, fetchImpl)
1376
+ };
1377
+ }
1378
+
1379
+ export {
1380
+ deriveSessionToken,
1381
+ deriveSessionTokenWithSigner,
1382
+ deriveSessionId,
1383
+ isLnAddress,
1384
+ isLnUrl,
1385
+ isValidLnUrl,
1386
+ toPayRequestUrl,
1387
+ isTokenAddress,
1388
+ tokenOptions,
1389
+ formatUnitAmount,
1390
+ parseUnitAmount,
1391
+ payableTokenUri,
1392
+ normalizeTokenQuote,
1393
+ resolve,
1394
+ requestInvoice,
1395
+ verifyStatusIsFor,
1396
+ pollVerify,
1397
+ batchVerify,
1398
+ openVerifyBatchStream,
1399
+ openSession,
1400
+ registerAddress,
1401
+ listAddresses,
1402
+ upgradeAddress,
1403
+ domainCapabilities,
1404
+ listPayments,
1405
+ revokeAddress,
1406
+ registerArkadeIdentity,
1407
+ listHdAllocations,
1408
+ syncPayments,
1409
+ browserPaymentStore,
1410
+ storedPayments,
1411
+ forgetStoredPayments,
1412
+ CONFIRMATION_TIMEOUT_MS,
1413
+ createPendingConfirmations,
1414
+ createLnurlPaymentRepository,
1415
+ clearLnurlPaymentsForReset,
1416
+ createLnurlClient
1417
+ };