thunder-bridge 1.4.2 → 2.1.1

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.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,69 +17,45 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AmountError: () => AmountError,
23
34
  GatewayCheatError: () => GatewayCheatError,
24
35
  Gateways: () => Gateways,
25
- IDEMPOTENCY_KEY_REUSED: () => IDEMPOTENCY_KEY_REUSED,
26
36
  IdempotencyConflictError: () => IdempotencyConflictError,
27
- NO_WALLET_AVAILABLE: () => NO_WALLET_AVAILABLE,
28
37
  NoWalletAvailableError: () => NoWalletAvailableError,
29
- PAYMENT_ALREADY_WATCHED: () => PAYMENT_ALREADY_WATCHED,
30
38
  ProblemError: () => ProblemError,
31
- REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
32
39
  ThunderBridge: () => ThunderBridge,
33
40
  UnverifiedRecipientError: () => UnverifiedRecipientError,
34
41
  WrapRefusedError: () => WrapRefusedError,
35
42
  answerVerifyChallenge: () => answerVerifyChallenge,
36
- answerVerifyChallengeRequest: () => answerVerifyChallengeRequest,
37
- answerWebhookChallenge: () => answerWebhookChallenge,
38
- answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
39
- bankRail: () => bankRail,
40
- bankTransfer: () => bankTransfer,
41
- bankVerifyEndpoint: () => bankVerifyEndpoint,
42
- bitstamp: () => bitstamp,
43
- coinbase: () => coinbase,
44
- coinmate: () => coinmate,
43
+ carriesProof: () => carriesProof,
45
44
  decodeInvoice: () => decodeInvoice,
46
- fioStatement: () => fioStatement,
47
- invoiceToDataUrl: () => invoiceToDataUrl,
48
- invoiceToSvg: () => invoiceToSvg,
49
- isProblemType: () => isProblemType,
50
- isProvablyPaid: () => isProvablyPaid,
51
- isProvablySettled: () => isProvablySettled,
52
- kraken: () => kraken,
53
- lightningRail: () => lightningRail,
54
- lnurlToDataUrl: () => lnurlToDataUrl,
55
- lnurlToSvg: () => lnurlToSvg,
56
- medianOf: () => medianOf,
57
- minorScaleOf: () => minorScaleOf,
58
- minorUnitsOf: () => minorUnitsOf,
59
- msatFor: () => msatFor,
60
- parseSettlement: () => parseSettlement,
61
- parseSettlementRequest: () => parseSettlementRequest,
62
- parseWatchedWebhook: () => parseWatchedWebhook,
63
- parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
64
- parseWebhook: () => parseWebhook,
65
- parseWebhookRequest: () => parseWebhookRequest,
45
+ fiat: () => fiat,
46
+ invoiceFrom: () => invoiceFrom,
47
+ msat: () => msat,
66
48
  preimageMatchesHash: () => preimageMatchesHash,
67
49
  proveOrigin: () => proveOrigin,
68
50
  proveSettlement: () => proveSettlement,
69
51
  proveWrapped: () => proveWrapped,
70
- qrToDataUrl: () => qrToDataUrl,
71
- qrToSvg: () => qrToSvg,
52
+ relayedVerifyUrl: () => relayedVerifyUrl,
53
+ sats: () => sats,
72
54
  seal: () => seal,
73
- spdToDataUrl: () => spdToDataUrl,
74
- spdToSvg: () => spdToSvg,
75
- toLnurl: () => toLnurl,
76
55
  unseal: () => unseal,
77
- verifyWebhookSignature: () => verifyWebhookSignature,
78
56
  wrapFeeCeiling: () => wrapFeeCeiling
79
57
  });
80
- module.exports = __toCommonJS(index_exports);
58
+ module.exports = __toCommonJS(src_exports);
81
59
 
82
60
  // ../core/bytes.ts
83
61
  function bytesToHex(bytes) {
@@ -94,8 +72,8 @@ function hexToBytes(hex) {
94
72
  }
95
73
  return bytes;
96
74
  }
97
- function isHex(text2) {
98
- return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
75
+ function isHex(text3) {
76
+ return /^[0-9a-f]+$/i.test(text3) && text3.length % 2 === 0;
99
77
  }
100
78
 
101
79
  // ../core/sha256.ts
@@ -189,8 +167,8 @@ function sha256(bytes) {
189
167
  }
190
168
  return asBytes(state);
191
169
  }
192
- function sha256Hex(text2) {
193
- return bytesToHex(sha256(new TextEncoder().encode(text2)));
170
+ function sha256Hex(text3) {
171
+ return bytesToHex(sha256(new TextEncoder().encode(text3)));
194
172
  }
195
173
  function withPadding(bytes) {
196
174
  const padded = new Uint8Array(
@@ -256,6 +234,7 @@ function rotate(value, by) {
256
234
 
257
235
  // ../core/bolt11.ts
258
236
  var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
237
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
259
238
  var CHECKSUM_WORDS = 6;
260
239
  var TIMESTAMP_WORDS = 7;
261
240
  var SIGNATURE_WORDS = 104;
@@ -273,18 +252,27 @@ var HRP_MULTIPLIER_MSAT = {
273
252
  n: MSAT_PER_BTC / 1e9,
274
253
  p: MSAT_PER_BTC / 1e12
275
254
  };
276
- var HRP_AMOUNT = /^ln[a-z]+?(\d+)([munp]?)$/;
255
+ var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
277
256
  function decodeInvoice(bolt11) {
278
257
  const parts = splitBech32(bolt11);
279
258
  if (parts === null) {
280
- return { paymentHash: null, descriptionHash: null, amountMsat: null, expiresAt: null };
259
+ return {
260
+ paymentHash: null,
261
+ descriptionHash: null,
262
+ amountMsat: null,
263
+ issuedAt: null,
264
+ expiresAt: null
265
+ };
281
266
  }
282
267
  const tagged = taggedFields(parts.words);
268
+ const issuedAt = issuedAtOf(parts.words);
269
+ const expiry = tagged.get(EXPIRY_TAG);
283
270
  return {
284
271
  paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
285
272
  descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
286
273
  amountMsat: amountFromHrp(parts.hrp),
287
- expiresAt: expiryOf(parts.words, tagged)
274
+ issuedAt,
275
+ expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
288
276
  };
289
277
  }
290
278
  function preimageMatchesHash(preimage, paymentHash) {
@@ -295,7 +283,7 @@ function preimageMatchesHash(preimage, paymentHash) {
295
283
  }
296
284
  function splitBech32(bolt11) {
297
285
  const lower = bolt11.toLowerCase();
298
- if (isBolt12Encoding(lower)) {
286
+ if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
299
287
  return null;
300
288
  }
301
289
  const separator = lower.lastIndexOf("1");
@@ -314,7 +302,28 @@ function splitBech32(bolt11) {
314
302
  }
315
303
  words.push(value);
316
304
  }
317
- return { hrp: lower.slice(0, separator), words: words.slice(0, -CHECKSUM_WORDS) };
305
+ const hrp = lower.slice(0, separator);
306
+ if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
307
+ return null;
308
+ }
309
+ return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
310
+ }
311
+ function expandedHrp(hrp) {
312
+ const codes = [...hrp].map((char) => char.charCodeAt(0));
313
+ return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
314
+ }
315
+ function bech32Polymod(values) {
316
+ let checksum = 1;
317
+ for (const value of values) {
318
+ const top = checksum >> 25;
319
+ checksum = (checksum & 33554431) << 5 ^ value;
320
+ for (let bit = 0; bit < 5; bit++) {
321
+ if (top >> bit & 1) {
322
+ checksum ^= BECH32_GENERATOR[bit];
323
+ }
324
+ }
325
+ }
326
+ return checksum;
318
327
  }
319
328
  function isBolt12Encoding(lower) {
320
329
  return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
@@ -344,19 +353,17 @@ function hexTag(fields, tag, expectedWords) {
344
353
  }
345
354
  return bytesToHex(wordsToBytes(words));
346
355
  }
347
- function expiryOf(words, fields) {
356
+ function issuedAtOf(words) {
348
357
  if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
349
358
  return null;
350
359
  }
351
- const expiry = fields.get(EXPIRY_TAG);
352
- return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
360
+ return readNumber(words.slice(0, TIMESTAMP_WORDS));
353
361
  }
354
362
  function amountFromHrp(hrp) {
355
- const match = HRP_AMOUNT.exec(hrp);
356
- if (match === null) {
363
+ const [, digits, multiplier] = HRP.exec(hrp) ?? [];
364
+ if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
357
365
  return null;
358
366
  }
359
- const [, digits, multiplier] = match;
360
367
  return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
361
368
  }
362
369
  function readNumber(words) {
@@ -377,180 +384,74 @@ function wordsToBytes(words) {
377
384
  return Uint8Array.from(bytes);
378
385
  }
379
386
 
380
- // ../core/url.ts
381
- var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
382
- function publicHttps(raw) {
383
- let url;
384
- try {
385
- url = new URL(raw);
386
- } catch {
387
- return false;
388
- }
389
- return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
390
- }
391
- function sameOrigin(one, other) {
392
- try {
393
- return new URL(one).origin === new URL(other).origin;
394
- } catch {
395
- return false;
396
- }
397
- }
398
- function publicAddress(literal) {
399
- return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
400
- }
401
- function hostIsPublic(host) {
402
- if (host.startsWith("[")) {
403
- return publicAddress(host.slice(1, -1));
404
- }
405
- const name = host.replace(/\.$/, "");
406
- if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
407
- return publicAddress(name);
408
- }
409
- const labels = name.split(".");
410
- if (labels.length < 2 || labels.some((label) => label === "")) {
411
- return false;
412
- }
413
- return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
414
- }
415
- function ipv4IsGlobal(literal) {
416
- const parts = literal.split(".").map(Number);
417
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
418
- return false;
419
- }
420
- const [a, b] = parts;
421
- if (a === 0 || a === 10 || a === 127 || a >= 240) {
422
- return false;
423
- }
424
- if (a === 100 && b >= 64 && b < 128) {
425
- return false;
426
- }
427
- if (a === 169 && b === 254) {
428
- return false;
429
- }
430
- if (a === 172 && b >= 16 && b < 32) {
431
- return false;
432
- }
433
- if (a === 192 && b === 168) {
434
- return false;
435
- }
436
- return true;
387
+ // ../core/hmac.ts
388
+ async function hmacHex(secret, payload) {
389
+ const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
390
+ const key = await crypto.subtle.importKey(
391
+ "raw",
392
+ asBuffer(raw),
393
+ { name: "HMAC", hash: "SHA-256" },
394
+ false,
395
+ ["sign"]
396
+ );
397
+ const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
398
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
437
399
  }
438
- function ipv6IsGlobal(literal) {
439
- const groups = ipv6Groups(literal.toLowerCase());
440
- if (groups.length !== 8 || groups.some(Number.isNaN)) {
441
- return false;
442
- }
443
- const leading = groups.slice(0, 5).every((group) => group === 0);
444
- if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
445
- return false;
446
- }
447
- if (leading && groups[5] === 65535) {
448
- const [high, low] = [groups[6], groups[7]];
449
- return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
450
- }
451
- const first = groups[0];
452
- if ((first & 65024) === 64512) {
453
- return false;
454
- }
455
- if ((first & 65472) === 65152) {
400
+ function equalInConstantTime(one, other) {
401
+ if (one.length !== other.length) {
456
402
  return false;
457
403
  }
458
- return true;
459
- }
460
- function ipv6Groups(address) {
461
- const [head = "", tail] = address.split("::");
462
- const left = head.split(":").filter((group) => group !== "");
463
- const right = (tail ?? "").split(":").filter((group) => group !== "");
464
- const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
465
- return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
466
- }
467
-
468
- // ../core/lnurl.ts
469
- var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
470
- var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
471
- var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
472
- var LNURL_HRP = "lnurl";
473
- function toLnurl(endpoint) {
474
- const scheme = parsedScheme(endpoint);
475
- if (scheme !== "https:" && scheme !== "http:") {
476
- throw new Error(`${endpoint} is not an http or https URL`);
477
- }
478
- const words = toWords(new TextEncoder().encode(endpoint));
479
- const data = [...words, ...checksumWords(LNURL_HRP, words)];
480
- return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
481
- }
482
- function parsedScheme(endpoint) {
483
- try {
484
- return new URL(endpoint).protocol;
485
- } catch {
486
- return null;
404
+ let difference = 0;
405
+ for (let at = 0; at < one.length; at++) {
406
+ difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
487
407
  }
408
+ return difference === 0;
488
409
  }
489
- function toWords(bytes) {
490
- const words = [];
491
- let accumulator = 0;
492
- let bits = 0;
493
- for (const byte of bytes) {
494
- accumulator = accumulator << 8 | byte;
495
- bits += 8;
496
- while (bits >= 5) {
497
- bits -= 5;
498
- words.push(accumulator >> bits & 31);
499
- }
500
- }
501
- if (bits > 0) {
502
- words.push(accumulator << 5 - bits & 31);
503
- }
504
- return words;
505
- }
506
- function checksumWords(hrp, words) {
507
- const expanded = [...hrp].map((char) => char.charCodeAt(0));
508
- const values = [
509
- ...expanded.map((code) => code >> 5),
510
- 0,
511
- ...expanded.map((code) => code & 31),
512
- ...words,
513
- ...CHECKSUM_SLOTS.map(() => 0)
514
- ];
515
- const polymod = bech32Polymod(values) ^ 1;
516
- return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
517
- }
518
- function bech32Polymod(values) {
519
- let checksum = 1;
520
- for (const value of values) {
521
- const top = checksum >> 25;
522
- checksum = (checksum & 33554431) << 5 ^ value;
523
- for (let bit = 0; bit < 5; bit++) {
524
- if (top >> bit & 1) {
525
- checksum ^= BECH32_GENERATOR[bit];
526
- }
527
- }
528
- }
529
- return checksum;
410
+ function asBuffer(bytes) {
411
+ return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
530
412
  }
531
413
 
532
414
  // ../core/sealed.ts
533
- var VERSION = "v1";
415
+ var VERSION = "v2";
416
+ var UNPADDED_VERSION = "v1";
534
417
  var IV_BYTES = 12;
535
418
  var MIN_SECRET_CHARS = 32;
536
419
  var MAX_PLAIN_BYTES = 3e3;
420
+ var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
421
+ var END_OF_TEXT = 128;
537
422
  var INFO = new TextEncoder().encode("thunder-bridge/sealed");
538
- async function seal(secret, plaintext) {
539
- const body = new TextEncoder().encode(plaintext);
540
- if (body.length > MAX_PLAIN_BYTES) {
541
- throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${body.length}`);
423
+ async function seal(secret, plaintext, paymentHash) {
424
+ return await sealUnder(
425
+ secret,
426
+ plaintext,
427
+ crypto.getRandomValues(new Uint8Array(IV_BYTES)),
428
+ boundTo(paymentHash)
429
+ );
430
+ }
431
+ async function sealStable(secret, plaintext) {
432
+ const derived = await hmacHex(secret, `sealed-nonce|${plaintext}`);
433
+ const nonce = new Uint8Array(IV_BYTES);
434
+ for (let at = 0; at < IV_BYTES; at++) {
435
+ nonce[at] = Number.parseInt(derived.slice(at * 2, at * 2 + 2), 16);
542
436
  }
437
+ return await sealUnder(secret, plaintext, nonce, boundTo(void 0));
438
+ }
439
+ async function sealUnder(secret, plaintext, iv, additionalData) {
440
+ const body = padToClass(plaintext);
543
441
  const key = await keyFor(secret);
544
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
545
- const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, body));
442
+ const cipher = new Uint8Array(
443
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
444
+ );
546
445
  const joined = new Uint8Array(iv.length + cipher.length);
547
446
  joined.set(iv);
548
447
  joined.set(cipher, iv.length);
549
448
  return `${VERSION}.${toBase64Url(joined)}`;
550
449
  }
551
- async function unseal(secret, sealed) {
450
+ async function unseal(secret, sealed, paymentHash) {
552
451
  const key = await keyFor(secret);
553
- if (!sealed.startsWith(`${VERSION}.`)) {
452
+ const padded = sealed.startsWith(`${VERSION}.`);
453
+ const unbound = !padded && sealed.startsWith(`${UNPADDED_VERSION}.`);
454
+ if (!padded && (!unbound || paymentHash !== void 0)) {
554
455
  return null;
555
456
  }
556
457
  const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
@@ -558,20 +459,50 @@ async function unseal(secret, sealed) {
558
459
  return null;
559
460
  }
560
461
  try {
561
- const body = await crypto.subtle.decrypt(
562
- { name: "AES-GCM", iv: joined.slice(0, IV_BYTES) },
563
- key,
564
- joined.slice(IV_BYTES)
462
+ const body = new Uint8Array(
463
+ await crypto.subtle.decrypt(
464
+ {
465
+ name: "AES-GCM",
466
+ iv: joined.slice(0, IV_BYTES),
467
+ additionalData: padded ? boundTo(paymentHash) : void 0
468
+ },
469
+ key,
470
+ joined.slice(IV_BYTES)
471
+ )
565
472
  );
566
- return new TextDecoder().decode(body);
473
+ const text3 = padded ? unpad(body) : body;
474
+ return text3 === null ? null : new TextDecoder().decode(text3);
567
475
  } catch {
568
476
  return null;
569
477
  }
570
478
  }
571
- async function keyFor(secret) {
479
+ function refuseAWeakSecret(secret) {
572
480
  if (secret.length < MIN_SECRET_CHARS) {
573
481
  throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
574
482
  }
483
+ }
484
+ function boundTo(paymentHash) {
485
+ return new TextEncoder().encode(
486
+ paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
487
+ );
488
+ }
489
+ function padToClass(plaintext) {
490
+ const text3 = new TextEncoder().encode(plaintext);
491
+ const size = SIZE_CLASSES.find((limit) => text3.length <= limit);
492
+ if (size === void 0) {
493
+ throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text3.length}`);
494
+ }
495
+ const body = new Uint8Array(size + 1);
496
+ body.set(text3);
497
+ body[text3.length] = END_OF_TEXT;
498
+ return body;
499
+ }
500
+ function unpad(body) {
501
+ const end = body.lastIndexOf(END_OF_TEXT);
502
+ return end === -1 ? null : body.subarray(0, end);
503
+ }
504
+ async function keyFor(secret) {
505
+ refuseAWeakSecret(secret);
575
506
  const material = await crypto.subtle.importKey(
576
507
  "raw",
577
508
  new TextEncoder().encode(secret),
@@ -594,12 +525,12 @@ function toBase64Url(bytes) {
594
525
  }
595
526
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
596
527
  }
597
- function fromBase64Url(text2) {
598
- if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) {
528
+ function fromBase64Url(text3) {
529
+ if (text3.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text3)) {
599
530
  return null;
600
531
  }
601
532
  try {
602
- const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
533
+ const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
603
534
  const bytes = new Uint8Array(binary.length);
604
535
  for (let at = 0; at < bytes.length; at++) {
605
536
  bytes[at] = binary.charCodeAt(at);
@@ -610,32 +541,109 @@ function fromBase64Url(text2) {
610
541
  }
611
542
  }
612
543
 
613
- // ../core/hmac.ts
614
- async function hmacHex(secret, payload) {
615
- const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
616
- const key = await crypto.subtle.importKey(
617
- "raw",
618
- asBuffer(raw),
619
- { name: "HMAC", hash: "SHA-256" },
620
- false,
621
- ["sign"]
622
- );
623
- const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
624
- return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
625
- }
626
- function equalInConstantTime(one, other) {
627
- if (one.length !== other.length) {
628
- return false;
544
+ // src/errors.ts
545
+ var AmountError = class extends Error {
546
+ /**
547
+ * Whether a failure is one of these, without asking whether it is this exact
548
+ * class. Every entry point carries its own copy of the class, so a price
549
+ * refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
550
+ * imported from `thunder-bridge`. The name and the code are the same in every
551
+ * copy, so this holds where `instanceof` does not
552
+ */
553
+ static is(failure) {
554
+ return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
629
555
  }
630
- let difference = 0;
631
- for (let at = 0; at < one.length; at++) {
632
- difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
556
+ code;
557
+ constructor(code, detail) {
558
+ super(`${code}: ${detail}`);
559
+ this.name = "AmountError";
560
+ this.code = code;
633
561
  }
634
- return difference === 0;
635
- }
636
- function asBuffer(bytes) {
637
- return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
562
+ };
563
+ var GatewayCheatError = class extends Error {
564
+ code;
565
+ paymentId;
566
+ constructor(code, paymentId) {
567
+ super(`gateway verification failed: ${code}`);
568
+ this.name = "GatewayCheatError";
569
+ this.code = code;
570
+ this.paymentId = paymentId;
571
+ }
572
+ };
573
+ var WrapRefusedError = class extends Error {
574
+ code;
575
+ constructor(code, detail) {
576
+ super(`wrapped invoice refused: ${code}, ${detail}`);
577
+ this.name = "WrapRefusedError";
578
+ this.code = code;
579
+ }
580
+ };
581
+ var UnverifiedRecipientError = class extends Error {
582
+ lnAddress;
583
+ paymentId;
584
+ constructor(lnAddress, paymentId, cause) {
585
+ super(`could not reach ${lnAddress} to verify the invoice: ${String(cause)}`);
586
+ this.name = "UnverifiedRecipientError";
587
+ this.lnAddress = lnAddress;
588
+ this.paymentId = paymentId;
589
+ }
590
+ };
591
+ var ProblemError = class extends Error {
592
+ static NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
593
+ static REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
594
+ static IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
595
+ static PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
596
+ static INVALID_REQUEST = "urn:problem-type:thunder-bridge:invalid-request";
597
+ static CALLER_UNKNOWN = "urn:problem-type:thunder-bridge:caller-unknown";
598
+ static VERIFY_HOST_REFUSED = "urn:problem-type:thunder-bridge:verify-host-refused";
599
+ static VERIFY_UNCONFIRMED = "urn:problem-type:thunder-bridge:verify-unconfirmed";
600
+ static VERIFY_UNCONSENTED = "urn:problem-type:thunder-bridge:verify-unconsented";
601
+ static WEBHOOK_UNCONFIRMED = "urn:problem-type:thunder-bridge:webhook-unconfirmed";
602
+ static TOO_MANY_PENDING = "urn:problem-type:thunder-bridge:too-many-pending";
603
+ /**
604
+ * Whether a problem carries this type. Branch on the type, never on the prose,
605
+ * and reach for `instanceof` first: every type this SDK gives a class to has one
606
+ */
607
+ static is(problem, type) {
608
+ return problem.type === type;
609
+ }
610
+ type;
611
+ title;
612
+ status;
613
+ detail;
614
+ constructor(problem) {
615
+ const title = problem.title ?? "Request failed";
616
+ super(problem.detail ? `${title}: ${problem.detail}` : title);
617
+ this.name = "ProblemError";
618
+ this.type = problem.type ?? "about:blank";
619
+ this.title = title;
620
+ this.status = problem.status ?? 0;
621
+ this.detail = problem.detail ?? null;
622
+ }
623
+ };
624
+ function isProblemType(problem, type) {
625
+ return problem.type === type;
638
626
  }
627
+ var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
628
+ var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
629
+ var IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
630
+ var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
631
+ var IdempotencyConflictError = class extends ProblemError {
632
+ conflict;
633
+ constructor(problem, conflict) {
634
+ super(problem);
635
+ this.name = "IdempotencyConflictError";
636
+ this.conflict = conflict;
637
+ }
638
+ };
639
+ var NoWalletAvailableError = class extends ProblemError {
640
+ wallets;
641
+ constructor(problem, wallets) {
642
+ super({ ...problem, type: NO_WALLET_AVAILABLE });
643
+ this.name = "NoWalletAvailableError";
644
+ this.wallets = wallets;
645
+ }
646
+ };
639
647
 
640
648
  // src/currency.ts
641
649
  var ISO_4217_MINOR_UNITS = {
@@ -669,7 +677,8 @@ var ISO_4217_MINOR_UNITS = {
669
677
  function minorUnitsOf(currency) {
670
678
  const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
671
679
  if (digits === void 0) {
672
- throw new Error(
680
+ throw new AmountError(
681
+ "unknown-currency",
673
682
  `${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
674
683
  );
675
684
  }
@@ -679,16 +688,218 @@ function minorScaleOf(currency) {
679
688
  return 10 ** minorUnitsOf(currency);
680
689
  }
681
690
 
682
- // ../core/ed25519.ts
683
- var ALGORITHM = { name: "Ed25519" };
684
- var SEED_BYTES = 32;
685
- var SIGNATURE_BYTES = 64;
686
- var PUBLIC_KEY_BYTES = 32;
687
- var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
688
- async function signingKeyFromSeed(seed) {
689
- if (seed.length !== SEED_BYTES) {
690
- throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
691
- }
691
+ // src/price.ts
692
+ var MSAT_PER_BTC2 = 1e11;
693
+ var BASIS_POINTS = 1e4;
694
+ var DEFAULT_HOLD_SECS = 60;
695
+ var DEFAULT_MIN_VENUES = 2;
696
+ var DEFAULT_MAX_SPREAD_BPS = 200;
697
+ var MILLIS = 1e3;
698
+ function coinbase(baseUrl = "https://api.coinbase.com") {
699
+ return async (currency) => {
700
+ const asked = currency.toUpperCase();
701
+ const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
702
+ const quoted2 = answer.data;
703
+ if (quoted2?.currency !== asked) {
704
+ throw new Error(`coinbase quoted ${quoted2?.currency} when asked for ${asked}`);
705
+ }
706
+ return asMinor(quoted2.amount, "coinbase", asked);
707
+ };
708
+ }
709
+ function kraken(baseUrl = "https://api.kraken.com") {
710
+ return async (currency) => {
711
+ const asked = currency.toUpperCase();
712
+ const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
713
+ if (answer.error?.length) {
714
+ throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
715
+ }
716
+ const pairs = Object.values(answer.result ?? {});
717
+ if (pairs.length !== 1) {
718
+ throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
719
+ }
720
+ return asMinor(pairs[0]?.c?.[0], "kraken", asked);
721
+ };
722
+ }
723
+ function bitstamp(baseUrl = "https://www.bitstamp.net") {
724
+ return async (currency) => {
725
+ const pair = `btc${currency.toLowerCase()}`;
726
+ const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
727
+ if (Array.isArray(answer)) {
728
+ throw new Error(`bitstamp has no ${pair} pair`);
729
+ }
730
+ return asMinor(answer.last, "bitstamp", currency);
731
+ };
732
+ }
733
+ function coinmate(baseUrl = "https://coinmate.io") {
734
+ return async (currency) => {
735
+ const pair = `BTC_${currency.toUpperCase()}`;
736
+ const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
737
+ if (answer.error !== false) {
738
+ throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
739
+ }
740
+ return asMinor(answer.data?.last, "coinmate", currency);
741
+ };
742
+ }
743
+ function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
744
+ const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
745
+ const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
746
+ const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
747
+ const held = /* @__PURE__ */ new Map();
748
+ return async (currency) => {
749
+ const asked = currency.toUpperCase();
750
+ const remembered = held.get(asked);
751
+ if (remembered && Date.now() - remembered.at < holdFor) {
752
+ return remembered.price;
753
+ }
754
+ const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
755
+ const quoted2 = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
756
+ if (quoted2.length < minVenues) {
757
+ throw new Error(
758
+ `${quoted2.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
759
+ );
760
+ }
761
+ const spread2 = spreadBpsOf(quoted2);
762
+ if (spread2 > maxSpread) {
763
+ throw new Error(
764
+ `venues disagree on ${asked} by ${Math.round(spread2)} bps, more than the ${maxSpread} allowed: ${quoted2.join(", ")}`
765
+ );
766
+ }
767
+ const price = middleOf(quoted2);
768
+ held.set(asked, { at: Date.now(), price });
769
+ return price;
770
+ };
771
+ }
772
+ function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
773
+ if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
774
+ throw new Error("amountMinor must be a whole number of minor units above zero");
775
+ }
776
+ if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
777
+ throw new Error(`${priceMinorPerBtc} is not a usable price`);
778
+ }
779
+ const spreadBps = options.spreadBps ?? 0;
780
+ if (!Number.isInteger(spreadBps) || spreadBps < 0) {
781
+ throw new Error("spreadBps must be a whole number of basis points, none or more");
782
+ }
783
+ const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
784
+ const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
785
+ const msat2 = (wanted + per - 1n) / per;
786
+ if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
787
+ throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
788
+ }
789
+ return Number(msat2);
790
+ }
791
+ async function asJson(url, venue) {
792
+ const answer = await fetch(url, { headers: { accept: "application/json" } });
793
+ if (!answer.ok) {
794
+ throw new Error(`${venue} answered ${answer.status}`);
795
+ }
796
+ return answer.json();
797
+ }
798
+ function asMinor(quoted2, venue, currency) {
799
+ const major2 = Number(quoted2);
800
+ if (!Number.isFinite(major2) || major2 <= 0) {
801
+ throw new Error(`${venue} quoted ${String(quoted2)} for ${currency.toUpperCase()}`);
802
+ }
803
+ return Math.round(major2 * minorScaleOf(currency));
804
+ }
805
+ function spreadBpsOf(sorted) {
806
+ const lowest = sorted[0] ?? 0;
807
+ const highest = sorted[sorted.length - 1] ?? 0;
808
+ return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
809
+ }
810
+ function middleOf(sorted) {
811
+ const middle = Math.floor(sorted.length / 2);
812
+ if (sorted.length % 2 === 1) {
813
+ return sorted[middle] ?? 0;
814
+ }
815
+ return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
816
+ }
817
+ function refusals(answers) {
818
+ return answers.filter((answer) => answer.status === "rejected").map(
819
+ (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
820
+ ).join("; ");
821
+ }
822
+
823
+ // src/amount.ts
824
+ var MSAT_PER_SAT = 1e3;
825
+ var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
826
+ function sats(whole) {
827
+ if (!Number.isInteger(whole) || whole < 1 || whole > MAX_SATS) {
828
+ throw new AmountError("not-whole-satoshi", `${whole} is not a payable count of satoshi`);
829
+ }
830
+ return whole * MSAT_PER_SAT;
831
+ }
832
+ function msat(exact) {
833
+ if (!Number.isSafeInteger(exact) || exact < 1) {
834
+ throw new AmountError(
835
+ "not-whole-millisatoshi",
836
+ `${exact} is not a payable count of millisatoshi`
837
+ );
838
+ }
839
+ return exact;
840
+ }
841
+ function fiat(major2, currency, options) {
842
+ const amountMinor = minorFrom(major2, currency);
843
+ const rate = options?.rate ?? medianOf();
844
+ return async () => {
845
+ const wanted = msatFor(amountMinor, await rate(currency), { spreadBps: options?.spreadBps });
846
+ return sats(Math.ceil(wanted / MSAT_PER_SAT));
847
+ };
848
+ }
849
+ async function amountNow(amount) {
850
+ return typeof amount === "function" ? msat(await amount()) : msat(amount);
851
+ }
852
+ function minorFrom(major2, currency) {
853
+ const digits = minorUnitsOf(currency);
854
+ if (typeof major2 === "number") {
855
+ return payable(Math.round(major2 * minorScaleOf(currency)), major2, currency);
856
+ }
857
+ const parsed = /^(\d+)(?:[.,](\d+))?$/.exec(major2.trim());
858
+ if (parsed === null) {
859
+ throw new AmountError(
860
+ "not-a-decimal",
861
+ `${major2} is not a decimal ${currency.toUpperCase()} amount`
862
+ );
863
+ }
864
+ const fraction = parsed[2] ?? "";
865
+ if (fraction.length > digits) {
866
+ throw new AmountError(
867
+ "too-precise",
868
+ `${major2} carries more decimals than ${currency.toUpperCase()} has, which is ${digits}`
869
+ );
870
+ }
871
+ return payable(Number(`${parsed[1]}${fraction.padEnd(digits, "0")}`), major2, currency);
872
+ }
873
+ function payable(minor, major2, currency) {
874
+ if (!Number.isSafeInteger(minor) || minor < 1) {
875
+ throw new AmountError(
876
+ "not-a-decimal",
877
+ `${major2} is not a ${currency.toUpperCase()} price this can charge`
878
+ );
879
+ }
880
+ return minor;
881
+ }
882
+
883
+ // ../core/ed25519.ts
884
+ var ALGORITHM = { name: "Ed25519" };
885
+ var SEED_BYTES = 32;
886
+ var SIGNATURE_BYTES = 64;
887
+ var PUBLIC_KEY_BYTES = 32;
888
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
889
+ var SIGN_BIT_CLEARED = 127;
890
+ var SMALL_ORDER = [
891
+ "00".repeat(32),
892
+ `01${"00".repeat(31)}`,
893
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
894
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
895
+ `ec${"ff".repeat(30)}7f`,
896
+ `ed${"ff".repeat(30)}7f`,
897
+ `ee${"ff".repeat(30)}7f`
898
+ ];
899
+ async function signingKeyFromSeed(seed) {
900
+ if (seed.length !== SEED_BYTES) {
901
+ throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
902
+ }
692
903
  const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
693
904
  wrapped.set(PKCS8_HEADER);
694
905
  wrapped.set(seed, PKCS8_HEADER.length);
@@ -703,6 +914,9 @@ async function verifyHex(publicKeyHex, signatureHex, payload) {
703
914
  if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) {
704
915
  return false;
705
916
  }
917
+ if (hasSmallOrder(publicKeyHex)) {
918
+ return false;
919
+ }
706
920
  if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) {
707
921
  return false;
708
922
  }
@@ -724,8 +938,13 @@ async function verifyHex(publicKeyHex, signatureHex, payload) {
724
938
  return false;
725
939
  }
726
940
  }
727
- function fromBase64Url2(text2) {
728
- const padded = text2.replace(/-/g, "+").replace(/_/g, "/");
941
+ function hasSmallOrder(publicKeyHex) {
942
+ const point = hexToBytes(publicKeyHex);
943
+ point[PUBLIC_KEY_BYTES - 1] = point[PUBLIC_KEY_BYTES - 1] & SIGN_BIT_CLEARED;
944
+ return SMALL_ORDER.includes(bytesToHex(point));
945
+ }
946
+ function fromBase64Url2(text3) {
947
+ const padded = text3.replace(/-/g, "+").replace(/_/g, "/");
729
948
  const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
730
949
  const bytes = new Uint8Array(raw.length);
731
950
  for (let at = 0; at < raw.length; at++) {
@@ -737,135 +956,766 @@ function asBuffer2(bytes) {
737
956
  return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
738
957
  }
739
958
 
740
- // src/wire.ts
741
- var ASSET_CODE = "BTC";
742
- var ASSET_SCALE = 11;
743
- var REASONS = /* @__PURE__ */ new Set([
744
- "address-unusable",
745
- "unreachable",
746
- "amount-not-accepted",
747
- "cannot-prove-delivery",
748
- "invoice-refused"
749
- ]);
750
- function createRequestBody(params, trigger, replay) {
751
- return JSON.stringify({
752
- ln_addresses: params.lnAddresses,
753
- incoming_amount: toAmount(params.amountMsat),
754
- webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0,
755
- trigger: trigger ?? void 0,
756
- replay
757
- });
959
+ // ../core/caller.ts
960
+ var KEY_HEADER = "x-client-key";
961
+ var SIGNATURE_HEADER = "x-signature";
962
+ var TIMESTAMP_HEADER = "x-timestamp";
963
+ var PREFIX = "ed25519=";
964
+ var SIGNING_LABEL = "request-signing";
965
+ async function callerKey(secret) {
966
+ return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
758
967
  }
759
- function quoteRequestBody(params) {
760
- return JSON.stringify({
761
- ln_addresses: params.lnAddresses,
762
- amount: toAmount(params.amountMsat)
763
- });
968
+ function paymentNamedBy(caller, paymentHash) {
969
+ return sha256Hex(`${caller}.payment-id.${paymentHash}`);
764
970
  }
765
- function quoteFromWire(body) {
766
- const wire = asObject(body);
767
- if (wire === null) {
768
- return null;
971
+ async function signedAs(key, method, path, body) {
972
+ const timestamp = String(Math.floor(Date.now() / 1e3));
973
+ return {
974
+ [KEY_HEADER]: key.publicKeyHex,
975
+ [TIMESTAMP_HEADER]: timestamp,
976
+ [SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
977
+ };
978
+ }
979
+ function spoken(timestamp, method, path, body) {
980
+ return new TextEncoder().encode(
981
+ `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
982
+ );
983
+ }
984
+
985
+ // src/charge.ts
986
+ async function priced(charge) {
987
+ const paidTo = typeof charge.paidTo === "string" ? [charge.paidTo] : [...charge.paidTo];
988
+ if (paidTo.length === 0) {
989
+ throw new Error("name at least one lightning address to pay");
769
990
  }
770
- const lnAddress = text(wire["ln_address"]);
771
- const amountMsat = msatFrom(wire["amount"], 1);
772
- const feeMsat = msatFrom(wire["fee"], 0);
773
- const minMsat = msatFrom(wire["min_amount"], 0);
774
- const maxMsat = msatFrom(wire["max_amount"], 0);
775
- const metadata = text(wire["metadata"]);
776
- const refusals3 = refusalsFrom(wire["refusals"]);
777
- if (lnAddress === null || amountMsat === null || feeMsat === null) {
778
- return null;
991
+ return { paidTo, amountMsat: await amountNow(charge.amount) };
992
+ }
993
+
994
+ // ../core/url.ts
995
+ var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
996
+ function publicHttps(raw) {
997
+ let url;
998
+ try {
999
+ url = new URL(raw);
1000
+ } catch {
1001
+ return false;
779
1002
  }
780
- if (minMsat === null || maxMsat === null || metadata === null || refusals3 === null) {
781
- return null;
1003
+ return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
1004
+ }
1005
+ function sameOrigin(one, other) {
1006
+ try {
1007
+ return new URL(one).origin === new URL(other).origin;
1008
+ } catch {
1009
+ return false;
782
1010
  }
783
- return { lnAddress, amountMsat, feeMsat, minMsat, maxMsat, metadata, refusals: refusals3 };
784
1011
  }
785
- function paymentFromWire(body) {
786
- const wire = asObject(body);
787
- if (wire === null) {
788
- return null;
1012
+ function publicAddress(literal) {
1013
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
1014
+ }
1015
+ function hostIsPublic(host) {
1016
+ if (host.startsWith("[")) {
1017
+ return publicAddress(host.slice(1, -1));
789
1018
  }
790
- const id = text(wire["id"]);
791
- const lnAddress = text(wire["ln_address"]);
792
- const bolt11 = text(wire["bolt11"]);
793
- const paymentHash = text(wire["payment_hash"]);
794
- const verifyUrl = text(wire["verify_url"]);
795
- const amountMsat = msatFrom(wire["incoming_amount"], 1);
796
- const expiresAt = secondsFrom(wire["expires_at"]);
797
- const createdAt = secondsFrom(wire["created_at"]);
798
- const status = wire["status"];
799
- const preimage = wire["preimage"] ?? null;
800
- if (id === null || lnAddress === null || bolt11 === null) {
801
- return null;
1019
+ const name = host.replace(/\.$/, "");
1020
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
1021
+ return publicAddress(name);
802
1022
  }
803
- if (paymentHash === null || verifyUrl === null) {
804
- return null;
1023
+ const labels = name.split(".");
1024
+ if (labels.length < 2 || labels.some((label) => label === "")) {
1025
+ return false;
805
1026
  }
806
- if (amountMsat === null || expiresAt === null || createdAt === null) {
807
- return null;
1027
+ return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
1028
+ }
1029
+ function ipv4IsGlobal(literal) {
1030
+ const parts = literal.split(".").map(Number);
1031
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
1032
+ return false;
808
1033
  }
809
- if (!isStatus(status)) {
810
- return null;
1034
+ const [a, b, c] = parts;
1035
+ if (a === 0 || a === 10 || a === 127 || a >= 224) {
1036
+ return false;
811
1037
  }
812
- if (preimage !== null && typeof preimage !== "string") {
813
- return null;
1038
+ if (a === 100 && b >= 64 && b < 128) {
1039
+ return false;
814
1040
  }
815
- return {
816
- id,
817
- lnAddress,
818
- amountMsat,
819
- status,
820
- paymentHash,
821
- bolt11,
822
- preimage,
823
- expiresAt,
824
- createdAt,
825
- verifyUrl
826
- };
827
- }
828
- function watchRequestBody(params, trigger) {
829
- const expiresAt = new Date(params.expiresAt * 1e3);
830
- if (Number.isNaN(expiresAt.getTime())) {
831
- throw new TypeError(`expiresAt must be a usable unix time in seconds, got ${params.expiresAt}`);
1041
+ if (a === 169 && b === 254) {
1042
+ return false;
1043
+ }
1044
+ if (a === 172 && b >= 16 && b < 32) {
1045
+ return false;
1046
+ }
1047
+ if (a === 192 && (b === 168 || b === 0 && (c === 0 || c === 2))) {
1048
+ return false;
1049
+ }
1050
+ if (a === 198 && (b === 18 || b === 19 || b === 51 && c === 100)) {
1051
+ return false;
1052
+ }
1053
+ if (a === 203 && b === 0 && c === 113) {
1054
+ return false;
1055
+ }
1056
+ return true;
1057
+ }
1058
+ function ipv6IsGlobal(literal) {
1059
+ const groups = ipv6Groups(literal.toLowerCase());
1060
+ if (groups.length !== 8 || groups.some(Number.isNaN)) {
1061
+ return false;
1062
+ }
1063
+ const [first, second, third, fourth, fifth, sixth, high, low] = groups;
1064
+ const embedded = (one, other) => ipv4IsGlobal(`${one >> 8}.${one & 255}.${other >> 8}.${other & 255}`);
1065
+ const zeroBefore = (count) => groups.slice(0, count).every((group) => group === 0);
1066
+ if (zeroBefore(5) && sixth === 65535) {
1067
+ return embedded(high, low);
1068
+ }
1069
+ if (zeroBefore(6) || zeroBefore(4) && fifth === 65535 && sixth === 0) {
1070
+ return false;
1071
+ }
1072
+ if (first === 100 && second === 65435) {
1073
+ return third === 0 && fourth === 0 && fifth === 0 && sixth === 0 && embedded(high, low);
1074
+ }
1075
+ if (first === 8194) {
1076
+ return embedded(second, third);
1077
+ }
1078
+ if (first === 8193 && second === 0) {
1079
+ return false;
1080
+ }
1081
+ if (first === 256 && second === 0 && third === 0 && fourth === 0) {
1082
+ return false;
1083
+ }
1084
+ if ((first & 65024) === 64512 || (first & 65280) === 65280) {
1085
+ return false;
1086
+ }
1087
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) {
1088
+ return false;
832
1089
  }
1090
+ return true;
1091
+ }
1092
+ function ipv6Groups(address) {
1093
+ const halves = withDottedTailAsGroups(address).split("::");
1094
+ const [head = "", tail] = halves;
1095
+ const left = head.split(":").filter((group) => group !== "");
1096
+ const right = (tail ?? "").split(":").filter((group) => group !== "");
1097
+ const missing = 8 - left.length - right.length;
1098
+ if (halves.length > 2 || missing < 0) {
1099
+ return [Number.NaN];
1100
+ }
1101
+ const gap = tail === void 0 ? [] : Array(missing).fill("0");
1102
+ return [...left, ...gap, ...right].map(
1103
+ (group) => /^[0-9a-f]{1,4}$/.test(group) ? parseInt(group, 16) : Number.NaN
1104
+ );
1105
+ }
1106
+ function withDottedTailAsGroups(address) {
1107
+ const dotted = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(address);
1108
+ if (dotted === null) {
1109
+ return address;
1110
+ }
1111
+ const [a, b, c, d] = dotted[2].split(".").map(Number);
1112
+ if ([a, b, c, d].some((part) => part > 255)) {
1113
+ return `${dotted[1]}x`;
1114
+ }
1115
+ return `${dotted[1]}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
1116
+ }
1117
+
1118
+ // ../core/outbound.ts
1119
+ var HTTP_TIMEOUT_MS = 15e3;
1120
+ var LOOKUP_TIMEOUT_MS = 5e3;
1121
+ var HOPS_FOLLOWED = 2;
1122
+ var REDIRECTS = [301, 302, 303, 307, 308];
1123
+ var KEEPS_THE_METHOD = [307, 308];
1124
+ var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
1125
+ var BODY_LIMIT_BYTES = 262144;
1126
+ async function ask(send, url, sent = {}) {
1127
+ const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
1128
+ const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
1129
+ let target = url;
1130
+ let carried = sent;
1131
+ for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
1132
+ const at = await addressesToReach(target);
1133
+ const response = await send(target, carried, signal, at);
1134
+ if (!REDIRECTS.includes(response.status)) {
1135
+ return await answerOf(response);
1136
+ }
1137
+ const location = response.headers.get("location");
1138
+ if (location === null) {
1139
+ throw new Error(`${target} answered ${response.status} without saying where to`);
1140
+ }
1141
+ await response.body?.cancel();
1142
+ const next = new URL(location, target).toString();
1143
+ if (sent.staysOnOrigin && !sameOrigin(target, next)) {
1144
+ throw new Error(`${target} redirected off its own origin, to ${next}`);
1145
+ }
1146
+ carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
1147
+ carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
1148
+ target = next;
1149
+ }
1150
+ throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
1151
+ }
1152
+ async function addressesToReach(url) {
1153
+ if (!publicHttps(url)) {
1154
+ throw new Error(`${url} is not a public https URL`);
1155
+ }
1156
+ const found = await resolved(url);
1157
+ if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
1158
+ throw new Error(`${url} resolves to an address we do not reach`);
1159
+ }
1160
+ return found;
1161
+ }
1162
+ async function resolved(url) {
1163
+ const { lookup } = await import("dns/promises");
1164
+ const { setTimeout: sleep } = await import("timers/promises");
1165
+ const host = new URL(url).hostname.replace(/^\[|]$/g, "");
1166
+ const found = await Promise.race([
1167
+ lookup(host, { all: true, verbatim: true }).catch(() => []),
1168
+ sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
1169
+ ]);
1170
+ return found ?? [];
1171
+ }
1172
+ async function answerOf(response) {
1173
+ const { status, ok, headers } = response;
1174
+ const reader = response.body?.getReader();
1175
+ if (!reader) {
1176
+ return { status, ok, body: "", truncated: false, headers };
1177
+ }
1178
+ const read = [];
1179
+ let bytes = 0;
1180
+ while (bytes <= BODY_LIMIT_BYTES) {
1181
+ const { done, value } = await reader.read();
1182
+ if (done) {
1183
+ return { status, ok, body: text(read), truncated: false, headers };
1184
+ }
1185
+ read.push(value);
1186
+ bytes += value.length;
1187
+ }
1188
+ await reader.cancel();
1189
+ return { status, ok, body: text(read), truncated: true, headers };
1190
+ }
1191
+ function withoutBody(sent) {
1192
+ const headers = Object.fromEntries(
1193
+ Object.entries(sent.headers ?? {}).filter(([name]) => name.toLowerCase() !== "content-type")
1194
+ );
1195
+ return { ...sent, method: "GET", headers, body: void 0 };
1196
+ }
1197
+ function withoutCredentials(sent) {
1198
+ const headers = Object.fromEntries(
1199
+ Object.entries(sent.headers ?? {}).filter(
1200
+ ([name]) => !CREDENTIALS.includes(name.toLowerCase())
1201
+ )
1202
+ );
1203
+ return { ...sent, headers };
1204
+ }
1205
+ function text(chunks) {
1206
+ const decoder = new TextDecoder();
1207
+ let decoded2 = "";
1208
+ for (const chunk of chunks) {
1209
+ decoded2 += decoder.decode(chunk, { stream: true });
1210
+ }
1211
+ return decoded2 + decoder.decode();
1212
+ }
1213
+
1214
+ // ../core/refusal.ts
1215
+ var WalletRefused = class extends Error {
1216
+ reason;
1217
+ constructor(reason, message) {
1218
+ super(message);
1219
+ this.reason = reason;
1220
+ }
1221
+ };
1222
+ var NoWalletAvailable = class extends Error {
1223
+ wallets;
1224
+ constructor(wallets) {
1225
+ super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
1226
+ this.wallets = wallets;
1227
+ }
1228
+ };
1229
+
1230
+ // ../core/lnurl.ts
1231
+ var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1232
+ var RESOLVE_TIMEOUT_MS = 3e4;
1233
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
1234
+ var MIN_PACE_SECS = 1;
1235
+ var MAX_PACE_SECS = 3600;
1236
+ var MIN_PER_SECOND = 0.01;
1237
+ var MAX_PER_SECOND = 100;
1238
+ async function resolve(send, addresses, amountMsat) {
1239
+ const served = await firstThatServes(
1240
+ addresses,
1241
+ (address, deadline) => resolveAddress(send, address, amountMsat, deadline)
1242
+ );
1243
+ return served.won;
1244
+ }
1245
+ async function quote(send, addresses, amountMsat) {
1246
+ const served = await firstThatServes(
1247
+ addresses,
1248
+ (address2, deadline) => probe(send, address2, amountMsat, deadline)
1249
+ );
1250
+ const { address, pay } = served.won;
1251
+ return {
1252
+ won: {
1253
+ address,
1254
+ minMsat: pay.minSendable,
1255
+ maxMsat: pay.maxSendable,
1256
+ metadata: pay.metadata
1257
+ },
1258
+ refusals: served.refusals
1259
+ };
1260
+ }
1261
+ async function firstThatServes(addresses, attempt) {
1262
+ const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
1263
+ const refusals3 = [];
1264
+ for (const address of addresses) {
1265
+ if (deadline.aborted) {
1266
+ break;
1267
+ }
1268
+ try {
1269
+ return { won: await attempt(address, deadline), refusals: refusals3 };
1270
+ } catch (refusal) {
1271
+ if (!(refusal instanceof WalletRefused)) {
1272
+ throw refusal;
1273
+ }
1274
+ console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
1275
+ refusals3.push({ address, reason: refusal.reason });
1276
+ }
1277
+ }
1278
+ throw new NoWalletAvailable(refusals3);
1279
+ }
1280
+ async function probe(send, address, amountMsat, deadline) {
1281
+ const [user, domain] = splitAddress(address);
1282
+ if (cannotReleaseAPreimage(domain)) {
1283
+ throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
1284
+ }
1285
+ const pay = await answered(
1286
+ send,
1287
+ `https://${domain}/.well-known/lnurlp/${user}`,
1288
+ deadline
1289
+ );
1290
+ if (pay.tag !== "payRequest" || !pay.callback) {
1291
+ throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
1292
+ }
1293
+ if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
1294
+ throw new WalletRefused(
1295
+ "amount-not-accepted",
1296
+ `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
1297
+ );
1298
+ }
1299
+ return { address, pay };
1300
+ }
1301
+ async function resolveAddress(send, address, amountMsat, deadline) {
1302
+ const { pay } = await probe(send, address, amountMsat, deadline);
1303
+ const invoice = await answered(
1304
+ send,
1305
+ withAmount(pay.callback, amountMsat),
1306
+ deadline
1307
+ );
1308
+ if (!invoice.pr) {
1309
+ throw new WalletRefused("unreachable", `${address} returned no invoice`);
1310
+ }
1311
+ if (!invoice.verify || !publicHttps(invoice.verify)) {
1312
+ throw new WalletRefused(
1313
+ "cannot-prove-delivery",
1314
+ `${address} publishes no usable LUD-21 verify URL`
1315
+ );
1316
+ }
1317
+ const issued = decodeIssued(address, invoice.pr);
1318
+ if (issued.amountMsat !== amountMsat) {
1319
+ throw new WalletRefused(
1320
+ "invoice-refused",
1321
+ `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
1322
+ );
1323
+ }
1324
+ if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
1325
+ throw new WalletRefused(
1326
+ "invoice-refused",
1327
+ `${address} does not bind its invoice to the metadata it serves`
1328
+ );
1329
+ }
1330
+ return {
1331
+ address,
1332
+ bolt11: invoice.pr,
1333
+ verifyUrl: invoice.verify,
1334
+ paymentHash: issued.paymentHash,
1335
+ expiresAt: issued.expiresAt
1336
+ };
1337
+ }
1338
+ async function answered(send, url, deadline) {
1339
+ try {
1340
+ return await fetchJson(send, url, deadline);
1341
+ } catch (cause) {
1342
+ throw new WalletRefused("unreachable", String(cause));
1343
+ }
1344
+ }
1345
+ function decodeIssued(address, bolt11) {
1346
+ const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
1347
+ if (paymentHash === null || expiresAt === null) {
1348
+ throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
1349
+ }
1350
+ return { paymentHash, descriptionHash, amountMsat, expiresAt };
1351
+ }
1352
+ async function checkSettled(send, verifyUrl, paymentHash) {
1353
+ const answer = await answeredJson(send, verifyUrl, { staysOnOrigin: true });
1354
+ const asked = {
1355
+ pace: paceAskedFor(answer.headers),
1356
+ ceiling: ceilingAskedFor(answer.headers)
1357
+ };
1358
+ if (!answer.said.settled || !answer.said.preimage) {
1359
+ return { preimage: null, ...asked };
1360
+ }
1361
+ if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
1362
+ throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
1363
+ }
1364
+ return { preimage: answer.said.preimage, ...asked };
1365
+ }
1366
+ function paceAskedFor(headers) {
1367
+ const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
1368
+ if (!asked) {
1369
+ return null;
1370
+ }
1371
+ return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
1372
+ }
1373
+ function ceilingAskedFor(headers) {
1374
+ const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
1375
+ if (!asked) {
1376
+ return null;
1377
+ }
1378
+ const perWindow = Number(asked[1]);
1379
+ const window = asked[2] === void 0 ? 1 : Number(asked[2]);
1380
+ if (perWindow === 0 || window === 0) {
1381
+ return null;
1382
+ }
1383
+ return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
1384
+ }
1385
+ function cannotReleaseAPreimage(host) {
1386
+ const lowered = host.toLowerCase();
1387
+ return VERIFY_WITHOUT_PREIMAGE.some(
1388
+ (known) => lowered === known || lowered.endsWith(`.${known}`)
1389
+ );
1390
+ }
1391
+ function withAmount(callback, amountMsat) {
1392
+ const separator = callback.includes("?") ? "&" : "?";
1393
+ return `${callback}${separator}amount=${amountMsat}`;
1394
+ }
1395
+ function splitAddress(address) {
1396
+ const [user, domain, ...rest] = address.split("@");
1397
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
1398
+ throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
1399
+ }
1400
+ return [user, domain];
1401
+ }
1402
+ function isNothingButAHost(domain) {
1403
+ try {
1404
+ const parsed = new URL(`https://${domain}/`);
1405
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
1406
+ } catch {
1407
+ return false;
1408
+ }
1409
+ }
1410
+ async function fetchJson(send, url, deadline) {
1411
+ return (await answeredJson(send, url, { deadline })).said;
1412
+ }
1413
+ async function answeredJson(send, url, sent) {
1414
+ const answer = await ask(send, url, { headers: { accept: "application/json" }, ...sent });
1415
+ if (!answer.ok) {
1416
+ throw new Error(`${url} answered ${answer.status}`);
1417
+ }
1418
+ if (answer.truncated) {
1419
+ throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
1420
+ }
1421
+ return { said: JSON.parse(answer.body), headers: answer.headers };
1422
+ }
1423
+
1424
+ // ../core/pinned.ts
1425
+ var HTTPS_PORT = 443;
1426
+ var CARRIES_NO_BODY = [204, 205, 304];
1427
+ var pinnedToTheAddressWeVerified = async (url, sent, signal, at) => {
1428
+ const asked = new URL(url);
1429
+ const first = at[0];
1430
+ if (first === void 0) {
1431
+ throw new Error(`${url} resolved to nothing worth connecting to`);
1432
+ }
1433
+ const { request } = await import("https");
1434
+ const { Readable } = await import("stream");
1435
+ return new Promise((settle, fail) => {
1436
+ if (signal.aborted) {
1437
+ fail(signal.reason);
1438
+ return;
1439
+ }
1440
+ const call = request(reaching(asked, sent, at, first), (answer) => {
1441
+ const status = answer.statusCode ?? 502;
1442
+ try {
1443
+ settle(
1444
+ new Response(
1445
+ CARRIES_NO_BODY.includes(status) ? null : Readable.toWeb(answer),
1446
+ { status, headers: headersOf(answer.headers) }
1447
+ )
1448
+ );
1449
+ } catch (unreadable) {
1450
+ answer.destroy();
1451
+ fail(new Error(`${url} answered with something HTTP cannot carry`, { cause: unreadable }));
1452
+ }
1453
+ });
1454
+ const give = () => call.destroy(new Error(`${url} took longer than allowed`));
1455
+ signal.addEventListener("abort", give, { once: true });
1456
+ call.on("close", () => signal.removeEventListener("abort", give));
1457
+ call.on("error", fail);
1458
+ if (sent.body !== void 0) {
1459
+ call.write(sent.body);
1460
+ }
1461
+ call.end();
1462
+ });
1463
+ };
1464
+ function reaching(asked, sent, at, first) {
1465
+ return {
1466
+ host: asked.hostname,
1467
+ servername: asked.hostname,
1468
+ port: asked.port === "" ? HTTPS_PORT : Number(asked.port),
1469
+ path: `${asked.pathname}${asked.search}`,
1470
+ method: sent.method ?? "GET",
1471
+ headers: { ...sent.headers, host: asked.host },
1472
+ agent: false,
1473
+ lookup: (_name, options, done) => {
1474
+ if (options.all === true) {
1475
+ done(
1476
+ null,
1477
+ at.map((one) => ({ ...one }))
1478
+ );
1479
+ } else {
1480
+ done(null, first.address, first.family);
1481
+ }
1482
+ }
1483
+ };
1484
+ }
1485
+ function headersOf(raw) {
1486
+ const headers = new Headers();
1487
+ for (const [name, value] of Object.entries(raw)) {
1488
+ if (Array.isArray(value)) {
1489
+ for (const one of value) {
1490
+ headers.append(name, one);
1491
+ }
1492
+ } else if (value !== void 0) {
1493
+ headers.set(name, value);
1494
+ }
1495
+ }
1496
+ return headers;
1497
+ }
1498
+
1499
+ // src/verify.ts
1500
+ var HTTP_TIMEOUT_MS2 = 15e3;
1501
+ var ISSUED_SKEW_SECS = 300;
1502
+ var NAMES_AN_ADDRESS = ["text/identifier", "text/email"];
1503
+ var DEFAULT_WRAP_PROPORTION = 0.01;
1504
+ var DEFAULT_WRAP_BASE_MSAT = 1e3;
1505
+ async function proveOrigin(payment, asked, askedAt) {
1506
+ const cheat = (code) => new GatewayCheatError(code, payment.id);
1507
+ const listed2 = asked.paidTo.find((address) => equalIgnoringCase(address, payment.lnAddress));
1508
+ if (listed2 === void 0) {
1509
+ throw cheat("address_not_requested");
1510
+ }
1511
+ if (payment.amountMsat !== asked.amountMsat) {
1512
+ throw cheat("amount_mismatch");
1513
+ }
1514
+ const invoice = decodeInvoice(payment.bolt11);
1515
+ if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
1516
+ throw cheat("hash_mismatch");
1517
+ }
1518
+ if (invoice.amountMsat !== asked.amountMsat) {
1519
+ throw cheat("amount_mismatch");
1520
+ }
1521
+ if (askedAt !== void 0 && (invoice.issuedAt ?? 0) < askedAt - ISSUED_SKEW_SECS) {
1522
+ throw cheat("invoice_stale");
1523
+ }
1524
+ const payRequest = await payRequestFor(listed2, payment);
1525
+ if (typeof payRequest.metadata !== "string" || typeof payRequest.callback !== "string") {
1526
+ throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "no payRequest served");
1527
+ }
1528
+ if (!namesTheAddress(payRequest.metadata, listed2)) {
1529
+ throw new UnverifiedRecipientError(
1530
+ payment.lnAddress,
1531
+ payment.id,
1532
+ "its metadata names no text/identifier for this address, so no invoice binds to it"
1533
+ );
1534
+ }
1535
+ if (invoice.descriptionHash !== sha256Hex(payRequest.metadata)) {
1536
+ throw cheat("description_hash_mismatch");
1537
+ }
1538
+ if (!sameOrigin(payment.verifyUrl, payRequest.callback)) {
1539
+ throw cheat("verify_url_foreign");
1540
+ }
1541
+ const issued = await reachable(payment.verifyUrl, payment);
1542
+ if (typeof issued.pr !== "string" || !equalIgnoringCase(issued.pr, payment.bolt11)) {
1543
+ throw cheat("invoice_not_issued");
1544
+ }
1545
+ if (askedAt !== void 0 && issued.settled === true) {
1546
+ throw cheat("invoice_settled");
1547
+ }
1548
+ }
1549
+ function namesTheAddress(metadata, address) {
1550
+ try {
1551
+ const entries = JSON.parse(metadata);
1552
+ return Array.isArray(entries) && entries.some(
1553
+ (entry) => Array.isArray(entry) && NAMES_AN_ADDRESS.includes(entry[0]) && typeof entry[1] === "string" && equalIgnoringCase(entry[1], address)
1554
+ );
1555
+ } catch {
1556
+ return false;
1557
+ }
1558
+ }
1559
+ async function proveSettlement(payment, asked) {
1560
+ await proveOrigin(payment, asked);
1561
+ const verified = await reachable(payment.verifyUrl, payment);
1562
+ if (verified.settled !== true || typeof verified.preimage !== "string") {
1563
+ return null;
1564
+ }
1565
+ if (!preimageMatchesHash(verified.preimage, payment.paymentHash)) {
1566
+ throw new GatewayCheatError("preimage_mismatch", payment.id);
1567
+ }
1568
+ return verified.preimage;
1569
+ }
1570
+ function carriesProof(report) {
1571
+ if (report.status !== "paid" || report.preimage === null) {
1572
+ return false;
1573
+ }
1574
+ if (report.bolt11 !== void 0 && report.bolt11 !== null) {
1575
+ const invoiceHash = decodeInvoice(report.bolt11).paymentHash;
1576
+ if (invoiceHash === null || !equalIgnoringCase(invoiceHash, report.paymentHash)) {
1577
+ return false;
1578
+ }
1579
+ }
1580
+ return preimageMatchesHash(report.preimage, report.paymentHash);
1581
+ }
1582
+ function wrapFeeCeiling(amountMsat, allowance) {
1583
+ const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
1584
+ return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
1585
+ }
1586
+ function proveWrapped(wrapped, recipient, allowance) {
1587
+ const offered = decodeInvoice(wrapped);
1588
+ const owed = decodeInvoice(recipient);
1589
+ const refuse2 = (code, detail) => {
1590
+ throw new WrapRefusedError(code, detail);
1591
+ };
1592
+ if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
1593
+ refuse2("undecodable", "the wrapped invoice carries no hash, amount or expiry");
1594
+ }
1595
+ if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
1596
+ refuse2("undecodable", "the recipient's invoice carries no hash, amount or expiry");
1597
+ }
1598
+ if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
1599
+ refuse2("hash_mismatch", "settling the wrap would not settle the recipient");
1600
+ }
1601
+ const asked = offered.amountMsat ?? 0;
1602
+ const due = owed.amountMsat ?? 0;
1603
+ if (asked < due) {
1604
+ refuse2("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
1605
+ }
1606
+ const ceiling = wrapFeeCeiling(due, allowance);
1607
+ if (asked - due > ceiling) {
1608
+ refuse2("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
1609
+ }
1610
+ if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
1611
+ refuse2("recipient_expires_first", "the operator could be paid and find it cannot deliver");
1612
+ }
1613
+ }
1614
+ async function payRequestFor(listed2, payment) {
1615
+ const at = listed2.indexOf("@");
1616
+ const name = listed2.slice(0, at);
1617
+ const domain = listed2.slice(at + 1).toLowerCase();
1618
+ if (at < 1 || !publicHttps(`https://${domain}/`)) {
1619
+ throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "not a usable address");
1620
+ }
1621
+ return reachable(`https://${domain}/.well-known/lnurlp/${name}`, payment);
1622
+ }
1623
+ async function reachable(url, payment) {
1624
+ try {
1625
+ if (!publicHttps(url)) {
1626
+ throw new Error(`${url} is not a public https URL`);
1627
+ }
1628
+ const response = await fetch(url, {
1629
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2),
1630
+ headers: { accept: "application/json" }
1631
+ });
1632
+ if (response.redirected && !sameOrigin(response.url, url)) {
1633
+ throw new Error(`redirected off its own origin, to ${response.url}`);
1634
+ }
1635
+ if (!response.ok) {
1636
+ throw new Error(`answered ${response.status}`);
1637
+ }
1638
+ const body = JSON.parse(await boundedText(response));
1639
+ if (body === null || typeof body !== "object") {
1640
+ throw new Error("answered with no JSON object");
1641
+ }
1642
+ return body;
1643
+ } catch (cause) {
1644
+ throw new UnverifiedRecipientError(payment.lnAddress, payment.id, cause);
1645
+ }
1646
+ }
1647
+ async function boundedText(response) {
1648
+ const reader = response.body?.getReader();
1649
+ const decoder = new TextDecoder();
1650
+ let text3 = "";
1651
+ let bytes = 0;
1652
+ for (let chunk = await reader?.read(); chunk && !chunk.done; chunk = await reader?.read()) {
1653
+ bytes += chunk.value.length;
1654
+ if (bytes > BODY_LIMIT_BYTES) {
1655
+ await reader?.cancel();
1656
+ throw new Error(`answered with more than the ${BODY_LIMIT_BYTES} bytes a proof reads`);
1657
+ }
1658
+ text3 += decoder.decode(chunk.value, { stream: true });
1659
+ }
1660
+ return text3 + decoder.decode();
1661
+ }
1662
+ function equalIgnoringCase(one, other) {
1663
+ return one.toLowerCase() === other.toLowerCase();
1664
+ }
1665
+
1666
+ // src/wire.ts
1667
+ var ASSET_CODE = "BTC";
1668
+ var ASSET_SCALE = 11;
1669
+ var REASONS = /* @__PURE__ */ new Set([
1670
+ "address-unusable",
1671
+ "unreachable",
1672
+ "amount-not-accepted",
1673
+ "cannot-prove-delivery",
1674
+ "invoice-refused"
1675
+ ]);
1676
+ function createRequestBody(priced2, webhookUrl, trigger, replay) {
833
1677
  return JSON.stringify({
834
- payment_hash: params.paymentHash,
835
- verify_url: params.verifyUrl,
836
- expires_at: expiresAt.toISOString(),
1678
+ ln_addresses: priced2.paidTo,
1679
+ incoming_amount: toAmount(priced2.amountMsat),
1680
+ webhook: webhookUrl ? { url: webhookUrl } : void 0,
837
1681
  trigger: trigger ?? void 0,
838
- replay: params.replay,
839
- sealed: params.sealed,
840
- webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0
1682
+ replay
841
1683
  });
842
1684
  }
843
- function settlementFromWire(body) {
1685
+ function quoteRequestBody(priced2) {
1686
+ return JSON.stringify({
1687
+ ln_addresses: priced2.paidTo,
1688
+ amount: toAmount(priced2.amountMsat)
1689
+ });
1690
+ }
1691
+ function quoteFromWire(body) {
844
1692
  const wire = asObject(body);
845
1693
  if (wire === null) {
846
1694
  return null;
847
1695
  }
848
- const id = text(wire["id"]);
849
- const paymentHash = text(wire["payment_hash"]);
850
- const settledAt = secondsFrom(wire["settled_at"]);
851
- const status = wire["status"];
852
- const preimage = wire["preimage"] ?? null;
853
- if (id === null || paymentHash === null || settledAt === null || !isStatus(status)) {
1696
+ const lnAddress = text2(wire["ln_address"]);
1697
+ const amountMsat = msatFrom(wire["amount"], 1);
1698
+ const feeMsat = msatFrom(wire["fee"], 0);
1699
+ const minMsat = msatFrom(wire["min_amount"], 0);
1700
+ const maxMsat = msatFrom(wire["max_amount"], 0);
1701
+ const metadata = text2(wire["metadata"]);
1702
+ const refusals3 = refusalsFrom(wire["refusals"]);
1703
+ if (lnAddress === null || amountMsat === null || feeMsat === null) {
854
1704
  return null;
855
1705
  }
856
- if (preimage !== null && typeof preimage !== "string") {
1706
+ if (minMsat === null || maxMsat === null || metadata === null || refusals3 === null) {
857
1707
  return null;
858
1708
  }
859
- return { id, status, paymentHash, preimage, settledAt };
1709
+ return { lnAddress, amountMsat, feeMsat, minMsat, maxMsat, metadata, refusals: refusals3 };
860
1710
  }
861
- function triggerEventFromWire(body) {
1711
+ function paymentFromWire(body) {
862
1712
  const wire = asObject(body);
863
1713
  if (wire === null) {
864
1714
  return null;
865
1715
  }
866
- const id = text(wire["id"]);
867
- const paymentHash = text(wire["payment_hash"]);
868
- const verifyUrl = text(wire["verify_url"]);
1716
+ const id = text2(wire["id"]);
1717
+ const paymentHash = text2(wire["payment_hash"]);
1718
+ const verifyUrl = text2(wire["verify_url"]);
869
1719
  const expiresAt = secondsFrom(wire["expires_at"]);
870
1720
  const createdAt = secondsFrom(wire["created_at"]);
871
1721
  const status = wire["status"];
@@ -883,41 +1733,80 @@ function triggerEventFromWire(body) {
883
1733
  if (sealed !== null && typeof sealed !== "string") {
884
1734
  return null;
885
1735
  }
886
- return {
1736
+ const reported = {
887
1737
  id,
888
- kind: kindOf(wire),
1738
+ status,
889
1739
  paymentHash,
890
1740
  verifyUrl,
891
- status,
892
1741
  preimage,
893
1742
  expiresAt,
894
1743
  createdAt,
895
- sealed,
896
- lnAddress: text(wire["ln_address"]),
897
- amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
1744
+ sealed
898
1745
  };
1746
+ const lnAddress = text2(wire["ln_address"]);
1747
+ const amountMsat = wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1);
1748
+ const bolt11 = text2(wire["bolt11"]);
1749
+ if (lnAddress !== null && amountMsat !== null && bolt11 !== null) {
1750
+ return { ...reported, kind: "minted", lnAddress, amountMsat, bolt11 };
1751
+ }
1752
+ if (lnAddress === null && amountMsat === null && bolt11 === null) {
1753
+ return { ...reported, kind: "watched", lnAddress: null, amountMsat: null, bolt11: null };
1754
+ }
1755
+ return null;
1756
+ }
1757
+ function mintedFromWire(body) {
1758
+ const payment = paymentFromWire(body);
1759
+ return payment !== null && payment.kind === "minted" ? payment : null;
1760
+ }
1761
+ function watchRequestBody(handover, trigger) {
1762
+ const expiresAt = new Date(handover.expiresAt * 1e3);
1763
+ if (Number.isNaN(expiresAt.getTime())) {
1764
+ throw new TypeError(
1765
+ `expiresAt must be a usable unix time in seconds, got ${handover.expiresAt}`
1766
+ );
1767
+ }
1768
+ return JSON.stringify({
1769
+ payment_hash: handover.paymentHash,
1770
+ verify_url: handover.verifyUrl,
1771
+ expires_at: expiresAt.toISOString(),
1772
+ trigger: trigger ?? void 0,
1773
+ replay: handover.replay,
1774
+ sealed: handover.sealed,
1775
+ webhook: handover.webhookUrl ? { url: handover.webhookUrl } : void 0
1776
+ });
1777
+ }
1778
+ function settlementFromWire(body) {
1779
+ const wire = asObject(body);
1780
+ if (wire === null) {
1781
+ return null;
1782
+ }
1783
+ const id = text2(wire["id"]);
1784
+ const paymentHash = text2(wire["payment_hash"]);
1785
+ const settledAt = secondsFrom(wire["settled_at"]);
1786
+ const status = wire["status"];
1787
+ const preimage = wire["preimage"] ?? null;
1788
+ if (id === null || paymentHash === null || settledAt === null || !isStatus(status)) {
1789
+ return null;
1790
+ }
1791
+ if (preimage !== null && typeof preimage !== "string") {
1792
+ return null;
1793
+ }
1794
+ return { id, status, paymentHash, preimage, settledAt };
899
1795
  }
900
1796
  function socketTicketFromWire(body) {
901
1797
  const wire = asObject(body);
902
1798
  if (wire === null) {
903
1799
  return null;
904
1800
  }
905
- const ticket = text(wire["ticket"]);
1801
+ const ticket = text2(wire["ticket"]);
906
1802
  const expiresAt = secondsFrom(wire["expires_at"]);
907
1803
  if (ticket === null || expiresAt === null) {
908
1804
  return null;
909
1805
  }
910
1806
  return { ticket, expiresAt };
911
1807
  }
912
- function kindOf(wire) {
913
- const said = wire["kind"];
914
- if (said === "minted" || said === "watched") {
915
- return said;
916
- }
917
- return text(wire["ln_address"]) === null ? "watched" : "minted";
918
- }
919
- function toAmount(msat) {
920
- return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
1808
+ function toAmount(msat2) {
1809
+ return { value: String(msat2), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
921
1810
  }
922
1811
  function asObject(value) {
923
1812
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -925,7 +1814,7 @@ function asObject(value) {
925
1814
  }
926
1815
  return value;
927
1816
  }
928
- function text(value) {
1817
+ function text2(value) {
929
1818
  return typeof value === "string" ? value : null;
930
1819
  }
931
1820
  function msatFrom(value, least) {
@@ -940,8 +1829,8 @@ function msatFrom(value, least) {
940
1829
  if (typeof digits !== "string" || !/^[0-9]+$/.test(digits)) {
941
1830
  return null;
942
1831
  }
943
- const msat = Number(digits);
944
- return Number.isSafeInteger(msat) && msat >= least ? msat : null;
1832
+ const msat2 = Number(digits);
1833
+ return Number.isSafeInteger(msat2) && msat2 >= least ? msat2 : null;
945
1834
  }
946
1835
  function refusalsFrom(value) {
947
1836
  if (!Array.isArray(value)) {
@@ -950,7 +1839,7 @@ function refusalsFrom(value) {
950
1839
  const refusals3 = [];
951
1840
  for (const entry of value) {
952
1841
  const failure = asObject(entry);
953
- const address = failure === null ? null : text(failure["address"]);
1842
+ const address = failure === null ? null : text2(failure["address"]);
954
1843
  const reason = failure?.["reason"];
955
1844
  if (address === null || typeof reason !== "string" || !REASONS.has(reason)) {
956
1845
  return null;
@@ -971,118 +1860,70 @@ function isStatus(value) {
971
1860
  }
972
1861
 
973
1862
  // src/webhook.ts
974
- var SIGNATURE_HEADER = "x-signature";
975
- var TIMESTAMP_HEADER = "x-timestamp";
1863
+ var SIGNATURE_HEADER2 = "x-signature";
1864
+ var TIMESTAMP_HEADER2 = "x-timestamp";
976
1865
  var GATEWAY_KEY_PREFIX = "ed25519=";
977
1866
  var DEFAULT_TOLERANCE_SECS = 300;
978
1867
  var CHALLENGE = "webhook-challenge";
979
1868
  var VERIFY_CHALLENGE = "verify-challenge";
980
- async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
981
- if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) {
982
- return false;
983
- }
984
- if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
985
- return false;
986
- }
987
- return await verifyHex(
988
- credential.publicKey.toLowerCase(),
989
- signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
990
- signed(timestamp, body)
991
- );
1869
+ async function readPayment(request, credential, options = {}) {
1870
+ const body = await believable(request, credential, options);
1871
+ return body === null ? null : proved(decoded(body, paymentFromWire));
992
1872
  }
993
- async function parseWebhook(body, signature, credential, timestamp, options = {}) {
994
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
995
- return null;
996
- }
997
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
998
- try {
999
- return paymentFromWire(JSON.parse(text2));
1000
- } catch {
1001
- return null;
1002
- }
1873
+ async function readSettlement(request, credential, options = {}) {
1874
+ const body = await believable(request, credential, options);
1875
+ return body === null ? null : proved(decoded(body, settlementFromWire));
1003
1876
  }
1004
- async function parseWebhookRequest(request, credential, options = {}) {
1005
- const signature = request.headers.get(SIGNATURE_HEADER);
1006
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1007
- if (signature === null || timestamp === null) {
1008
- return null;
1009
- }
1010
- return parseWebhook(await request.text(), signature, credential, timestamp, options);
1877
+ async function answerWebhookChallenge(request, credential, options = {}) {
1878
+ const body = await believable(request, credential, options);
1879
+ const nonce = body === null ? null : nonceOf(body, CHALLENGE);
1880
+ return nonce === null ? null : answered2(nonce);
1011
1881
  }
1012
- async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1013
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
1014
- return null;
1015
- }
1016
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1017
- try {
1018
- return triggerEventFromWire(JSON.parse(text2));
1019
- } catch {
1882
+ async function answerVerifyChallenge(request) {
1883
+ if (request.method !== "POST") {
1020
1884
  return null;
1021
1885
  }
1886
+ const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
1887
+ return nonce === null ? null : answered2(nonce);
1022
1888
  }
1023
- async function parseWatchedWebhookRequest(request, credential, options = {}) {
1024
- const signature = request.headers.get(SIGNATURE_HEADER);
1025
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1889
+ async function believable(request, credential, options) {
1890
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1891
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1026
1892
  if (signature === null || timestamp === null) {
1027
1893
  return null;
1028
1894
  }
1029
- return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1030
- }
1031
- async function parseSettlement(body, signature, credential, timestamp, options = {}) {
1032
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
1033
- return null;
1034
- }
1035
- try {
1036
- return settlementFromWire(JSON.parse(textOf(body)));
1037
- } catch {
1895
+ if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) {
1038
1896
  return null;
1039
1897
  }
1040
- }
1041
- async function parseSettlementRequest(request, credential, options = {}) {
1042
- const signature = request.headers.get(SIGNATURE_HEADER);
1043
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1044
- if (signature === null || timestamp === null) {
1898
+ if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
1045
1899
  return null;
1046
1900
  }
1047
- return await parseSettlement(await request.text(), signature, credential, timestamp, options);
1048
- }
1049
- function isProvablySettled(settled) {
1050
- if (settled.status !== "paid" || settled.preimage === null) {
1051
- return false;
1052
- }
1053
- return preimageMatchesHash(settled.preimage, settled.paymentHash);
1901
+ const body = await request.clone().text();
1902
+ const signed = await verifyHex(
1903
+ credential.publicKey.toLowerCase(),
1904
+ signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1905
+ stamped(timestamp, body)
1906
+ );
1907
+ return signed ? body : null;
1054
1908
  }
1055
- async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1056
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
1057
- return null;
1058
- }
1059
- const nonce = challenged(typeof body === "string" ? body : new TextDecoder().decode(body));
1060
- return nonce === null ? null : JSON.stringify({ nonce });
1909
+ function proved(read) {
1910
+ return read !== null && read.status === "paid" && !carriesProof(read) ? null : read;
1061
1911
  }
1062
- async function answerWebhookChallengeRequest(request, credential, options = {}) {
1063
- const signature = request.headers.get(SIGNATURE_HEADER);
1064
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1065
- if (signature === null || timestamp === null) {
1912
+ function decoded(body, from) {
1913
+ try {
1914
+ return from(JSON.parse(body));
1915
+ } catch {
1066
1916
  return null;
1067
1917
  }
1068
- const answer = await answerWebhookChallenge(
1069
- await request.clone().text(),
1070
- signature,
1071
- credential,
1072
- timestamp,
1073
- options
1074
- );
1075
- return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1076
1918
  }
1077
- function textOf(body) {
1078
- return typeof body === "string" ? body : new TextDecoder().decode(body);
1079
- }
1080
- function challenged(text2) {
1081
- return nonceOf(text2, CHALLENGE);
1919
+ function answered2(nonce) {
1920
+ return new Response(JSON.stringify({ nonce }), {
1921
+ headers: { "content-type": "application/json" }
1922
+ });
1082
1923
  }
1083
- function nonceOf(text2, type) {
1924
+ function nonceOf(text3, type) {
1084
1925
  try {
1085
- const said = JSON.parse(text2);
1926
+ const said = JSON.parse(text3);
1086
1927
  if (said["type"] !== type || typeof said["nonce"] !== "string") {
1087
1928
  return null;
1088
1929
  }
@@ -1091,17 +1932,6 @@ function nonceOf(text2, type) {
1091
1932
  return null;
1092
1933
  }
1093
1934
  }
1094
- function answerVerifyChallenge(body) {
1095
- const nonce = nonceOf(body, VERIFY_CHALLENGE);
1096
- return nonce === null ? null : JSON.stringify({ nonce });
1097
- }
1098
- async function answerVerifyChallengeRequest(request) {
1099
- if (request.method !== "POST") {
1100
- return null;
1101
- }
1102
- const answer = answerVerifyChallenge(await request.clone().text());
1103
- return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1104
- }
1105
1935
  function recent(timestamp, toleranceSecs) {
1106
1936
  const sent = Number(timestamp);
1107
1937
  if (!Number.isFinite(sent)) {
@@ -1109,14 +1939,8 @@ function recent(timestamp, toleranceSecs) {
1109
1939
  }
1110
1940
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1111
1941
  }
1112
- function signed(timestamp, body) {
1113
- const encoder = new TextEncoder();
1114
- const stamp = encoder.encode(`${timestamp}.`);
1115
- const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
1116
- const payload = new Uint8Array(stamp.length + rest.length);
1117
- payload.set(stamp);
1118
- payload.set(rest, stamp.length);
1119
- return payload;
1942
+ function stamped(timestamp, body) {
1943
+ return new TextEncoder().encode(`${timestamp}.${body}`);
1120
1944
  }
1121
1945
 
1122
1946
  // src/bank.ts
@@ -1125,83 +1949,120 @@ var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
1125
1949
  var DEFAULT_POLL_EVERY_SECS = 30;
1126
1950
  var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
1127
1951
  var FORBIDDEN_IN_SPD = "*";
1128
- async function bankTransfer(params) {
1952
+ var MAX_REFERENCE_CHARS = 60;
1953
+ async function bankTransfer(gateway, params) {
1129
1954
  const currency = params.currency ?? DEFAULT_CURRENCY;
1130
- refuseUnusable(params, currency);
1131
- await refuseAnOpenGateway(params);
1132
- const spd = shortPaymentDescriptor(params, currency);
1133
- const subject = subjectOf(params.reference, params.amountMinor, currency);
1134
- const polling = new URL(params.verifyUrl);
1135
- polling.searchParams.set("ref", params.reference);
1136
- polling.searchParams.set("minor", String(params.amountMinor));
1137
- polling.searchParams.set("cc", currency);
1138
- polling.searchParams.set("sig", await hmacHex(params.secret, `verify|${subject}`));
1139
- const verifyUrl = polling.toString();
1140
- const watched = await params.gateway.watchPayment({
1141
- paymentHash: hashOf(await hmacHex(params.secret, `preimage|${subject}`)),
1955
+ const iban = accountOf(params.iban);
1956
+ refuseUnusable(params, iban, currency);
1957
+ await refuseAnOpenGateway(gateway, params);
1958
+ const spd = shortPaymentDescriptor(params, iban, currency);
1959
+ const subject = subjectOf({
1960
+ iban,
1961
+ amountMinor: params.amountMinor,
1962
+ currency,
1963
+ expiresAt: params.expiresAt,
1964
+ reference: params.reference
1965
+ });
1966
+ const verifyUrl = await answeredAt(params, subject);
1967
+ const paymentHash = hashOf(await hmacHex(params.secret, `preimage|${subject}`));
1968
+ const watched = await gateway.watch({
1969
+ paymentHash,
1142
1970
  verifyUrl,
1143
1971
  expiresAt: params.expiresAt,
1144
1972
  trigger: params.trigger,
1145
1973
  replay: params.replay,
1146
- sealed: params.sealed,
1974
+ sealed: params.sealed ? await seal(params.sealed.secret, JSON.stringify(params.sealed.data), paymentHash) : void 0,
1147
1975
  webhookUrl: params.webhookUrl
1148
1976
  });
1149
- return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
1977
+ return { id: watched.id, paymentHash, verifyUrl, spd };
1150
1978
  }
1151
1979
  function bankVerifyEndpoint(config) {
1980
+ refuseAWeakSecret(config.secret);
1981
+ const answersFor = accountOf(config.iban);
1982
+ if (!IBAN.test(answersFor)) {
1983
+ throw new Error(`${config.iban} is not an IBAN, so this endpoint answers for no account`);
1984
+ }
1152
1985
  const paced = {
1153
1986
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1154
1987
  };
1155
1988
  return async (request) => {
1156
- const consented = await answerVerifyChallengeRequest(request);
1989
+ const consented = await answerVerifyChallenge(request);
1157
1990
  if (consented !== null) {
1158
1991
  return consented;
1159
1992
  }
1160
- const asked = readQuery(new URL(request.url));
1161
- if (asked === null) {
1993
+ const sealed = new URL(request.url).searchParams.get("q");
1994
+ if (sealed === null) {
1162
1995
  return Response.json({ settled: false }, { status: 400 });
1163
1996
  }
1164
- const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
1165
- const expected = await hmacHex(config.secret, `verify|${subject}`);
1166
- if (!equalInConstantTime(asked.signature, expected)) {
1997
+ const subject = await unseal(config.secret, sealed);
1998
+ const asked = subject === null ? null : askedFrom(subject);
1999
+ if (asked === null || asked.iban !== answersFor) {
1167
2000
  return Response.json({ settled: false }, { status: 403 });
1168
2001
  }
1169
- const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
1170
- const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
1171
- if (!landed) {
1172
- return Response.json({ settled: false }, { headers: paced });
1173
- }
1174
- return Response.json(
1175
- { settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
1176
- { headers: paced }
1177
- );
2002
+ const preimage = await creditedPreimage({
2003
+ secret: config.secret,
2004
+ asked,
2005
+ statement: config.statement,
2006
+ lookBackSecs: config.lookBackSecs
2007
+ });
2008
+ return preimage === null ? Response.json({ settled: false }, { headers: paced }) : Response.json({ settled: true, preimage }, { headers: paced });
1178
2009
  };
1179
2010
  }
1180
- function readQuery(url) {
1181
- const reference = url.searchParams.get("ref");
1182
- const minor = Number(url.searchParams.get("minor"));
1183
- const currency = url.searchParams.get("cc");
1184
- const signature = url.searchParams.get("sig");
1185
- if (!reference || !currency || !signature) {
2011
+ async function answeredAt(params, subject) {
2012
+ if (params.answerBy === "agent") {
2013
+ return `agent:${(await callerKey(params.secret)).publicKeyHex}`;
2014
+ }
2015
+ const polling = new URL(params.verifyUrl ?? "");
2016
+ polling.searchParams.set("q", await sealStable(params.secret, subject));
2017
+ return polling.toString();
2018
+ }
2019
+ async function creditedPreimage(params) {
2020
+ const since = unixNow() - (params.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
2021
+ const landed = (await params.statement(since)).some((credit) => pays(credit, params.asked));
2022
+ if (!landed) {
2023
+ return null;
2024
+ }
2025
+ return await hmacHex(params.secret, `preimage|${subjectOf(params.asked)}`);
2026
+ }
2027
+ function askedFrom(subject) {
2028
+ const [iban, minorText, currency, expiresText, ...rest] = subject.split("|");
2029
+ const reference = rest.join("|");
2030
+ const amountMinor = Number(minorText);
2031
+ const expiresAt = Number(expiresText);
2032
+ if (!iban || !currency || reference.length === 0) {
1186
2033
  return null;
1187
2034
  }
1188
- if (!Number.isInteger(minor) || minor <= 0) {
2035
+ if (!Number.isInteger(amountMinor) || amountMinor <= 0 || !Number.isSafeInteger(expiresAt)) {
1189
2036
  return null;
1190
2037
  }
1191
- return { reference, amountMinor: minor, currency, signature };
2038
+ return { iban, reference, amountMinor, currency, expiresAt };
1192
2039
  }
1193
2040
  function pays(credit, asked) {
1194
- return credit.amountMinor === asked.amountMinor && credit.currency.toUpperCase() === asked.currency.toUpperCase() && credit.reference.toUpperCase().includes(asked.reference.toUpperCase());
2041
+ return credit.amountMinor === asked.amountMinor && credit.currency.toUpperCase() === asked.currency.toUpperCase() && namesOnly(credit.reference, asked.reference);
2042
+ }
2043
+ function namesOnly(written, reference) {
2044
+ const wanted = reference.toUpperCase();
2045
+ const alike = [...written.toUpperCase().matchAll(wordsShapedLike(wanted))].map(([word]) => word);
2046
+ return alike.includes(wanted) && (!/\d/.test(wanted) || alike.every((word) => word === wanted));
2047
+ }
2048
+ function wordsShapedLike(reference) {
2049
+ const shape = [...reference].map((char) => {
2050
+ if (/\d/.test(char)) {
2051
+ return "\\d";
2052
+ }
2053
+ return /\p{L}/u.test(char) ? "\\p{L}" : char.replace(/[\^$\\.*+?()[\]{}|/]/g, "\\$&");
2054
+ }).join("");
2055
+ return new RegExp(`(?<![\\p{L}\\p{N}])${shape}(?![\\p{L}\\p{N}])`, "gu");
1195
2056
  }
1196
- function subjectOf(reference, amountMinor, currency) {
1197
- return `${reference}|${amountMinor}|${currency.toUpperCase()}`;
2057
+ function subjectOf({ iban, amountMinor, currency, expiresAt, reference }) {
2058
+ return `${iban}|${amountMinor}|${currency.toUpperCase()}|${expiresAt}|${reference}`;
1198
2059
  }
1199
2060
  function hashOf(preimage) {
1200
2061
  return bytesToHex(sha256(hexToBytes(preimage)));
1201
2062
  }
1202
- function shortPaymentDescriptor(params, currency) {
2063
+ function shortPaymentDescriptor(params, iban, currency) {
1203
2064
  const fields = [
1204
- `ACC:${params.iban}`,
2065
+ `ACC:${iban}`,
1205
2066
  `AM:${major(params.amountMinor, currency)}`,
1206
2067
  `CC:${currency.toUpperCase()}`,
1207
2068
  `MSG:${params.reference}`
@@ -1214,27 +2075,36 @@ function shortPaymentDescriptor(params, currency) {
1214
2075
  function major(amountMinor, currency) {
1215
2076
  return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
1216
2077
  }
1217
- async function refuseAnOpenGateway(params) {
2078
+ async function refuseAnOpenGateway(gateway, params) {
1218
2079
  if (params.allowPublicGateway === true) {
1219
2080
  return;
1220
2081
  }
1221
- if (await params.gateway.refusesStrangers()) {
2082
+ if (await gateway.refusesStrangers()) {
1222
2083
  return;
1223
2084
  }
1224
2085
  throw new Error(
1225
- "this gateway serves callers with no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Point at one that answers 401 to a stranger, or say allowPublicGateway"
2086
+ "this gateway serves callers with no token, so it is not yours, and its operator would learn every watch you place. Point at one that answers 401 to a stranger, or say allowPublicGateway"
1226
2087
  );
1227
2088
  }
1228
- function refuseUnusable(params, currency) {
1229
- if (!IBAN.test(params.iban)) {
2089
+ function refuseUnusable(params, iban, currency) {
2090
+ refuseAWeakSecret(params.secret);
2091
+ if (params.answerBy !== "agent" && params.verifyUrl === void 0) {
2092
+ throw new Error("a transfer the gateway polls needs the verify url it is polled at");
2093
+ }
2094
+ if (!IBAN.test(iban)) {
1230
2095
  throw new Error(`${params.iban} is not an IBAN`);
1231
2096
  }
1232
- if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
2097
+ if (!Number.isSafeInteger(params.amountMinor) || params.amountMinor <= 0) {
1233
2098
  throw new Error("amountMinor must be a whole number of minor units above zero");
1234
2099
  }
1235
2100
  if (params.reference.length === 0) {
1236
2101
  throw new Error("a transfer with no reference cannot be found");
1237
2102
  }
2103
+ if (params.reference.length > MAX_REFERENCE_CHARS) {
2104
+ throw new Error(
2105
+ `a reference is at most ${MAX_REFERENCE_CHARS} characters, as many as a QR payment's message carries and its sealed query hides`
2106
+ );
2107
+ }
1238
2108
  if (params.reference.includes(FORBIDDEN_IN_SPD)) {
1239
2109
  throw new Error("a reference cannot contain an asterisk, it separates the QR fields");
1240
2110
  }
@@ -1242,226 +2112,533 @@ function refuseUnusable(params, currency) {
1242
2112
  throw new Error("a variable symbol is up to ten digits");
1243
2113
  }
1244
2114
  }
2115
+ function accountOf(iban) {
2116
+ return iban.replace(/\s+/g, "").toUpperCase();
2117
+ }
1245
2118
  function unixNow() {
1246
2119
  return Math.floor(Date.now() / 1e3);
1247
2120
  }
1248
2121
 
1249
- // ../core/caller.ts
1250
- var KEY_HEADER = "x-client-key";
1251
- var SIGNATURE_HEADER2 = "x-signature";
1252
- var TIMESTAMP_HEADER2 = "x-timestamp";
1253
- var PREFIX = "ed25519=";
1254
- var SIGNING_LABEL = "request-signing";
1255
- async function callerKey(secret) {
1256
- return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
2122
+ // src/qr.ts
2123
+ var import_uqr = require("uqr");
2124
+ var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
2125
+ function toLightningUri(destination) {
2126
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
2127
+ return `LIGHTNING:${scanned}`;
1257
2128
  }
1258
- function paymentNamedBy(caller, paymentHash) {
1259
- return sha256Hex(`${caller}.payment-id.${paymentHash}`);
2129
+ function invoiceToSvg(destination, options) {
2130
+ return svgOf(toLightningUri(destination), options);
1260
2131
  }
1261
- async function signedAs(key, method, path, body) {
1262
- const timestamp = String(Math.floor(Date.now() / 1e3));
1263
- return {
1264
- [KEY_HEADER]: key.publicKeyHex,
1265
- [TIMESTAMP_HEADER2]: timestamp,
1266
- [SIGNATURE_HEADER2]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
2132
+ function svgOf(scanned, options) {
2133
+ const size = options?.size ?? 256;
2134
+ const color = options?.color ?? "#000";
2135
+ if (!SAFE_COLOR.test(color)) {
2136
+ throw new Error(`Invalid color: ${color}`);
2137
+ }
2138
+ return renderSvg(scanned, size, color);
2139
+ }
2140
+ function isLnAddress(destination) {
2141
+ const at = destination.indexOf("@");
2142
+ return at > 0 && destination.slice(at + 1).includes(".");
2143
+ }
2144
+ function renderSvg(content, size, color) {
2145
+ const { data } = (0, import_uqr.encode)(content);
2146
+ const modules = data.length;
2147
+ const cellSize = size / modules;
2148
+ let path = "";
2149
+ for (let y = 0; y < modules; y++) {
2150
+ for (let x = 0; x < modules; x++) {
2151
+ if (data[y][x]) {
2152
+ path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
2153
+ }
2154
+ }
2155
+ }
2156
+ return [
2157
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
2158
+ `<rect width="100%" height="100%" fill="white"/>`,
2159
+ `<path d="${path}" fill="${color}"/>`,
2160
+ `</svg>`
2161
+ ].join("");
2162
+ }
2163
+
2164
+ // src/relay.ts
2165
+ var DEFAULT_POLL_EVERY_SECS2 = 5;
2166
+ var WALLET = "w";
2167
+ function lightningVerifyEndpoint(config) {
2168
+ const paced = {
2169
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
2170
+ };
2171
+ return async (request) => {
2172
+ const consented = await answerVerifyChallenge(request);
2173
+ if (consented !== null) {
2174
+ return consented;
2175
+ }
2176
+ const sealed = new URL(request.url).searchParams.get(WALLET);
2177
+ if (sealed === null) {
2178
+ return Response.json({ settled: false }, { status: 400 });
2179
+ }
2180
+ const opened = await unseal(config.secret, sealed);
2181
+ const wallet = opened === null ? null : relayedFrom(opened);
2182
+ if (wallet === null) {
2183
+ return Response.json({ settled: false }, { status: 403 });
2184
+ }
2185
+ const asked = await checkSettled(
2186
+ config.send ?? pinnedToTheAddressWeVerified,
2187
+ wallet.url,
2188
+ wallet.hash
2189
+ ).catch(() => null);
2190
+ if (asked === null) {
2191
+ return Response.json({ settled: false }, { status: 502 });
2192
+ }
2193
+ return Response.json(
2194
+ { settled: asked.preimage !== null, preimage: asked.preimage },
2195
+ { headers: paced }
2196
+ );
1267
2197
  };
1268
2198
  }
1269
- function spoken(timestamp, method, path, body) {
1270
- return new TextEncoder().encode(
1271
- `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
1272
- );
2199
+ function relayedFrom(opened) {
2200
+ try {
2201
+ const said = JSON.parse(opened);
2202
+ return typeof said?.url === "string" && typeof said.hash === "string" ? { url: said.url, hash: said.hash } : null;
2203
+ } catch {
2204
+ return null;
2205
+ }
2206
+ }
2207
+ async function relayedVerifyUrl(endpoint, wallet, secret) {
2208
+ const relayed = new URL(endpoint);
2209
+ relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
2210
+ return relayed.toString();
1273
2211
  }
1274
2212
 
1275
- // src/errors.ts
1276
- var GatewayCheatError = class extends Error {
1277
- code;
1278
- paymentId;
1279
- constructor(code, paymentId) {
1280
- super(`gateway verification failed: ${code}`);
1281
- this.name = "GatewayCheatError";
1282
- this.code = code;
1283
- this.paymentId = paymentId;
2213
+ // src/rail.ts
2214
+ var BANK = "bank";
2215
+ var LIGHTNING = "lightning";
2216
+ function bankRail(gateway, config) {
2217
+ return async (order) => {
2218
+ const expiresAt = config.expiresAt(order);
2219
+ const transfer = await bankTransfer(gateway, {
2220
+ secret: config.secret,
2221
+ iban: config.iban,
2222
+ verifyUrl: config.verifyUrl,
2223
+ reference: order.reference,
2224
+ amountMinor: order.amountMinor,
2225
+ currency: order.currency,
2226
+ expiresAt,
2227
+ trigger: config.trigger,
2228
+ replay: config.replay,
2229
+ sealed: config.sealed && { secret: config.sealed.secret, data: config.sealed.data(order) },
2230
+ variableSymbol: config.variableSymbol?.(order),
2231
+ webhookUrl: config.webhookUrl,
2232
+ allowPublicGateway: config.allowPublicGateway
2233
+ });
2234
+ return {
2235
+ id: transfer.id,
2236
+ paymentHash: transfer.paymentHash,
2237
+ rail: config.name ?? BANK,
2238
+ scan: transfer.spd,
2239
+ qr: transfer.spd,
2240
+ expiresAt
2241
+ };
2242
+ };
2243
+ }
2244
+ function lightningRail(gateway, config) {
2245
+ return async (order) => {
2246
+ const payment = await gateway.mint(
2247
+ {
2248
+ paidTo: config.paidTo,
2249
+ amount: await msatForOrder(order, config.amount, config.rate),
2250
+ webhookUrl: config.webhookUrl
2251
+ },
2252
+ {
2253
+ idempotencyKey: config.idempotencyKey?.(order),
2254
+ trigger: config.trigger,
2255
+ replay: config.replay
2256
+ }
2257
+ );
2258
+ return {
2259
+ id: payment.id,
2260
+ paymentHash: payment.paymentHash,
2261
+ rail: config.name ?? LIGHTNING,
2262
+ scan: payment.bolt11,
2263
+ qr: toLightningUri(payment.bolt11),
2264
+ expiresAt: payment.expiresAt
2265
+ };
2266
+ };
2267
+ }
2268
+ function blindLightningRail(gateway, config) {
2269
+ return async (order) => {
2270
+ const resolved2 = await invoiceFrom(
2271
+ config.paidTo,
2272
+ await msatForOrder(order, config.amount, config.rate),
2273
+ config.send
2274
+ );
2275
+ const relay = config.relayThrough;
2276
+ const watched = await gateway.watch({
2277
+ paymentHash: resolved2.paymentHash,
2278
+ verifyUrl: relay ? await relayedVerifyUrl(
2279
+ relay.endpoint,
2280
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
2281
+ relay.secret
2282
+ ) : resolved2.verifyUrl,
2283
+ expiresAt: resolved2.expiresAt,
2284
+ trigger: config.trigger,
2285
+ replay: config.replay,
2286
+ sealed: config.sealed ? await seal(
2287
+ config.sealed.secret,
2288
+ JSON.stringify(config.sealed.data(order)),
2289
+ resolved2.paymentHash
2290
+ ) : void 0,
2291
+ webhookUrl: config.webhookUrl
2292
+ });
2293
+ return {
2294
+ id: watched.id,
2295
+ paymentHash: resolved2.paymentHash,
2296
+ rail: config.name ?? LIGHTNING,
2297
+ scan: resolved2.bolt11,
2298
+ qr: toLightningUri(resolved2.bolt11),
2299
+ expiresAt: resolved2.expiresAt
2300
+ };
2301
+ };
2302
+ }
2303
+ async function invoiceFrom(paidTo, amount, send = pinnedToTheAddressWeVerified) {
2304
+ const addresses = typeof paidTo === "string" ? [paidTo] : paidTo;
2305
+ try {
2306
+ return await resolve(send, addresses, await amountNow(amount));
2307
+ } catch (refused) {
2308
+ if (refused instanceof NoWalletAvailable) {
2309
+ throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
2310
+ }
2311
+ throw refused;
1284
2312
  }
1285
- };
1286
- var WrapRefusedError = class extends Error {
1287
- code;
1288
- constructor(code, detail) {
1289
- super(`wrapped invoice refused: ${code}, ${detail}`);
1290
- this.name = "WrapRefusedError";
1291
- this.code = code;
2313
+ }
2314
+ async function msatForOrder(order, amount, rate) {
2315
+ if (amount !== void 0) {
2316
+ return await amountNow(amount(order));
1292
2317
  }
1293
- };
1294
- var UnverifiedRecipientError = class extends Error {
1295
- lnAddress;
1296
- paymentId;
1297
- constructor(lnAddress, paymentId, cause) {
1298
- super(`could not reach ${lnAddress} to verify the invoice: ${String(cause)}`);
1299
- this.name = "UnverifiedRecipientError";
1300
- this.lnAddress = lnAddress;
1301
- this.paymentId = paymentId;
2318
+ return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
2319
+ }
2320
+ var Rails = class {
2321
+ constructor(gateway) {
2322
+ this.gateway = gateway;
1302
2323
  }
1303
- };
1304
- var ProblemError = class extends Error {
1305
- type;
1306
- title;
1307
- status;
1308
- detail;
1309
- constructor(problem) {
1310
- const title = problem.title ?? "Request failed";
1311
- super(problem.detail ? `${title}: ${problem.detail}` : title);
1312
- this.name = "ProblemError";
1313
- this.type = problem.type ?? "about:blank";
1314
- this.title = title;
1315
- this.status = problem.status ?? 0;
1316
- this.detail = problem.detail ?? null;
2324
+ gateway;
2325
+ /** Lightning, with the gateway minting against a priority list of addresses */
2326
+ lightning(config) {
2327
+ return lightningRail(this.gateway, config);
1317
2328
  }
1318
- };
1319
- function isProblemType(problem, type) {
1320
- return problem.type === type;
1321
- }
1322
- var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
1323
- var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
1324
- var IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
1325
- var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
1326
- var IdempotencyConflictError = class extends ProblemError {
1327
- conflict;
1328
- constructor(problem, conflict) {
1329
- super(problem);
1330
- this.name = "IdempotencyConflictError";
1331
- this.conflict = conflict;
2329
+ /**
2330
+ * Lightning, with the invoice resolved here so the gateway is told neither the
2331
+ * address nor the amount
2332
+ */
2333
+ blindLightning(config) {
2334
+ return blindLightningRail(this.gateway, config);
1332
2335
  }
1333
- };
1334
- var NoWalletAvailableError = class extends ProblemError {
1335
- wallets;
1336
- constructor(problem, wallets) {
1337
- super({ ...problem, type: NO_WALLET_AVAILABLE });
1338
- this.name = "NoWalletAvailableError";
1339
- this.wallets = wallets;
2336
+ /** A bank transfer, proved the way a Lightning payment is */
2337
+ bank(config) {
2338
+ return bankRail(this.gateway, config);
2339
+ }
2340
+ /**
2341
+ * One bank transfer without building a rail first, for a shop that asks for
2342
+ * them one at a time rather than beside another payment method
2343
+ */
2344
+ transfer(params) {
2345
+ return bankTransfer(this.gateway, params);
1340
2346
  }
1341
2347
  };
1342
2348
 
1343
- // src/verify.ts
1344
- var HTTP_TIMEOUT_MS = 15e3;
1345
- var DEFAULT_WRAP_PROPORTION = 0.01;
1346
- var DEFAULT_WRAP_BASE_MSAT = 1e3;
1347
- async function proveOrigin(payment, request) {
1348
- const cheat = (code) => new GatewayCheatError(code, payment.id);
1349
- const listed = request.lnAddresses.find(
1350
- (address) => equalIgnoringCase(address, payment.lnAddress)
1351
- );
1352
- if (listed === void 0) {
1353
- throw cheat("address_not_requested");
1354
- }
1355
- if (payment.amountMsat !== request.amountMsat) {
1356
- throw cheat("amount_mismatch");
1357
- }
1358
- const invoice = decodeInvoice(payment.bolt11);
1359
- if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
1360
- throw cheat("hash_mismatch");
2349
+ // src/request.ts
2350
+ function paymentRequestOf(gateway, payment, options) {
2351
+ return {
2352
+ id: payment.id,
2353
+ bolt11: payment.bolt11,
2354
+ paymentHash: payment.paymentHash,
2355
+ lnAddress: payment.lnAddress,
2356
+ amountMsat: payment.amountMsat,
2357
+ expiresAt: payment.expiresAt,
2358
+ qr: invoiceToSvg(payment.bolt11, options?.qr),
2359
+ payment,
2360
+ paid: async (waiting) => paidOnly(await gateway.settled(payment, { ...waitingOf(options), ...waiting })),
2361
+ prove: () => proveSettlement(payment, { paidTo: [payment.lnAddress], amountMsat: payment.amountMsat }),
2362
+ onPaid: (arrived, failed) => {
2363
+ const stop = new AbortController();
2364
+ const waiting = waitingOf(options);
2365
+ const signal = waiting.signal ? AbortSignal.any([stop.signal, waiting.signal]) : stop.signal;
2366
+ gateway.settled(payment, { ...waiting, signal }).then((ended) => arrived(paidOnly(ended))).catch((reason) => {
2367
+ if (!stop.signal.aborted) {
2368
+ failed?.(reason);
2369
+ }
2370
+ });
2371
+ return () => stop.abort();
2372
+ }
2373
+ };
2374
+ }
2375
+ function paidOnly(ended) {
2376
+ if (ended.status !== "paid") {
2377
+ throw new Error(`payment ${ended.id} ended ${ended.status} rather than paid`);
1361
2378
  }
1362
- if (invoice.amountMsat !== request.amountMsat) {
1363
- throw cheat("amount_mismatch");
2379
+ if (ended.kind !== "minted") {
2380
+ throw new Error(`payment ${ended.id} came back without the invoice it was minted with`);
1364
2381
  }
1365
- const payRequest = await payRequestFor(listed, payment);
1366
- if (typeof payRequest.metadata !== "string" || typeof payRequest.callback !== "string") {
1367
- throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "no payRequest served");
2382
+ return ended;
2383
+ }
2384
+ function waitingOf(options) {
2385
+ return { signal: options?.signal, tickets: options?.tickets };
2386
+ }
2387
+
2388
+ // src/trigger.ts
2389
+ var NONCE_BYTES = 16;
2390
+ function lnurlPayEndpoint(gateway, config) {
2391
+ return async (request) => {
2392
+ const url = new URL(request.url);
2393
+ const asked = url.searchParams.get("to");
2394
+ try {
2395
+ return asked === null ? await offer(gateway, config, url) : await mint(gateway, config, url, asked);
2396
+ } catch (failure) {
2397
+ return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
2398
+ }
2399
+ };
2400
+ }
2401
+ function watchTicketEndpoint(gateway, config) {
2402
+ return async (request) => {
2403
+ const offered = await offeredSecret(request);
2404
+ if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
2405
+ return Response.json({ reason: "not the watch secret" }, { status: 403 });
2406
+ }
2407
+ return await issue(gateway, config);
2408
+ };
2409
+ }
2410
+ function publicWatchTicketEndpoint(gateway, config) {
2411
+ return () => issue(gateway, config);
2412
+ }
2413
+ async function offer(gateway, config, url) {
2414
+ const { least, most } = await spread(config.amount);
2415
+ const winner = await quoted(gateway, config, least);
2416
+ const nonce = randomNonce();
2417
+ const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
2418
+ callback.searchParams.set("to", winner.address);
2419
+ callback.searchParams.set("least", String(least));
2420
+ callback.searchParams.set("most", String(most));
2421
+ callback.searchParams.set("n", nonce);
2422
+ callback.searchParams.set("sig", await sign(config.secret, winner.address, least, most, nonce));
2423
+ return Response.json({
2424
+ tag: "payRequest",
2425
+ callback: callback.toString(),
2426
+ metadata: winner.metadata,
2427
+ minSendable: least,
2428
+ maxSendable: most
2429
+ });
2430
+ }
2431
+ async function quoted(gateway, config, amountMsat) {
2432
+ if (config.blind) {
2433
+ const { won } = await quote(
2434
+ config.send ?? pinnedToTheAddressWeVerified,
2435
+ listed(config.paidTo),
2436
+ amountMsat
2437
+ );
2438
+ return won;
1368
2439
  }
1369
- if (invoice.descriptionHash !== sha256Hex(payRequest.metadata)) {
1370
- throw cheat("description_hash_mismatch");
2440
+ const { lnAddress, metadata } = await gateway.quote({
2441
+ paidTo: config.paidTo,
2442
+ amount: msat(amountMsat)
2443
+ });
2444
+ return { address: lnAddress, metadata };
2445
+ }
2446
+ async function spread(amount) {
2447
+ if (typeof amount === "number" || typeof amount === "function") {
2448
+ const pinned = await amountNow(amount);
2449
+ return { least: pinned, most: pinned };
2450
+ }
2451
+ const least = await amountNow(amount.least);
2452
+ const most = await amountNow(amount.most);
2453
+ if (most < least) {
2454
+ throw new Error(`a range cannot end at ${most} msat when it starts at ${least}`);
2455
+ }
2456
+ return { least, most };
2457
+ }
2458
+ async function mint(gateway, config, url, address) {
2459
+ const least = Number(url.searchParams.get("least"));
2460
+ const most = Number(url.searchParams.get("most"));
2461
+ const nonce = url.searchParams.get("n") ?? "";
2462
+ const signature = url.searchParams.get("sig") ?? "";
2463
+ const expected = await sign(config.secret, address, least, most, nonce);
2464
+ if (!equalInConstantTime(signature, expected)) {
2465
+ return refuse("this callback was not signed here");
2466
+ }
2467
+ if (!listed(config.paidTo).some((one) => one.toLowerCase() === address.toLowerCase())) {
2468
+ return refuse(`${address} is not paid through this trigger`);
2469
+ }
2470
+ const asked = url.searchParams.get("amount");
2471
+ const amountMsat = asked === null ? least : Number(asked);
2472
+ if (!Number.isSafeInteger(amountMsat) || amountMsat < least || amountMsat > most) {
2473
+ return refuse(
2474
+ least === most ? `this trigger costs exactly ${least} msat` : `this trigger takes between ${least} and ${most} msat`
2475
+ );
1371
2476
  }
1372
- if (!sameOrigin(payment.verifyUrl, payRequest.callback)) {
1373
- throw cheat("verify_url_foreign");
2477
+ const minted = config.blind ? await mintBlind(gateway, config, address, amountMsat) : await mintThroughGateway(gateway, config, address, amountMsat, nonce);
2478
+ return Response.json({
2479
+ status: "OK",
2480
+ pr: minted.bolt11,
2481
+ routes: [],
2482
+ verify: minted.verifyUrl
2483
+ });
2484
+ }
2485
+ async function mintThroughGateway(gateway, config, address, amountMsat, nonce) {
2486
+ const payment = await gateway.mint(
2487
+ { paidTo: address, amount: msat(amountMsat) },
2488
+ { idempotencyKey: nonce, trigger: config.watchSecret, replay: config.replay }
2489
+ );
2490
+ return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
2491
+ }
2492
+ async function mintBlind(gateway, config, address, amountMsat) {
2493
+ const resolved2 = await resolve(
2494
+ config.send ?? pinnedToTheAddressWeVerified,
2495
+ [address],
2496
+ amountMsat
2497
+ );
2498
+ const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
2499
+ const locked = config.sealed;
2500
+ const relay = config.relayThrough;
2501
+ try {
2502
+ await gateway.watch({
2503
+ paymentHash: resolved2.paymentHash,
2504
+ verifyUrl: relay ? await relayedVerifyUrl(
2505
+ relay.endpoint,
2506
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
2507
+ relay.secret
2508
+ ) : resolved2.verifyUrl,
2509
+ expiresAt: resolved2.expiresAt,
2510
+ trigger: config.watchSecret,
2511
+ replay: config.replay,
2512
+ sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted)), minted.paymentHash) : void 0
2513
+ });
2514
+ } catch (refused) {
2515
+ if (!alreadyWatched(refused)) {
2516
+ throw refused;
2517
+ }
1374
2518
  }
1375
- const issued = await reachable(payment.verifyUrl, payment);
1376
- if (typeof issued.pr !== "string" || !equalIgnoringCase(issued.pr, payment.bolt11)) {
1377
- throw cheat("invoice_not_issued");
2519
+ return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
2520
+ }
2521
+ function listed(paidTo) {
2522
+ return typeof paidTo === "string" ? [paidTo] : paidTo;
2523
+ }
2524
+ function alreadyWatched(refused) {
2525
+ return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
2526
+ }
2527
+ function sign(secret, address, least, most, nonce) {
2528
+ return hmacHex(secret, `${address}|${least}|${most}|${nonce}`);
2529
+ }
2530
+ function randomNonce() {
2531
+ const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
2532
+ let hex = "";
2533
+ for (const byte of bytes) {
2534
+ hex += byte.toString(16).padStart(2, "0");
1378
2535
  }
2536
+ return hex;
1379
2537
  }
1380
- async function proveSettlement(payment, request) {
1381
- await proveOrigin(payment, request);
1382
- const verified = await reachable(payment.verifyUrl, payment);
1383
- if (verified.settled !== true || typeof verified.preimage !== "string") {
1384
- return null;
2538
+ function refuse(reason) {
2539
+ return Response.json({ status: "ERROR", reason });
2540
+ }
2541
+ async function issue(gateway, config) {
2542
+ const issued = await gateway.ticket(config.watchSecret, { replay: config.replay });
2543
+ return Response.json({
2544
+ ticket: issued.ticket,
2545
+ expires_at: new Date(issued.expiresAt * 1e3).toISOString()
2546
+ });
2547
+ }
2548
+ async function offeredSecret(request) {
2549
+ const body = await request.json().catch(() => null);
2550
+ return typeof body?.secret === "string" ? body.secret : "";
2551
+ }
2552
+
2553
+ // src/serving.ts
2554
+ var Serve = class {
2555
+ constructor(gateway) {
2556
+ this.gateway = gateway;
1385
2557
  }
1386
- if (!preimageMatchesHash(verified.preimage, payment.paymentHash)) {
1387
- throw new GatewayCheatError("preimage_mismatch", payment.id);
2558
+ gateway;
2559
+ /**
2560
+ * An LNURL-pay endpoint of your own, standing in front of a priority list of
2561
+ * addresses, so a printed QR points at your domain and never expires
2562
+ */
2563
+ lnurlPay(config) {
2564
+ return lnurlPayEndpoint(this.gateway, config);
1388
2565
  }
1389
- return verified.preimage;
1390
- }
1391
- function isProvablyPaid(payment) {
1392
- if (payment.status !== "paid" || payment.preimage === null) {
1393
- return false;
2566
+ /** Trades the watch secret for a one minute socket ticket, refusing anyone without it */
2567
+ watchTicket(config) {
2568
+ return watchTicketEndpoint(this.gateway, config);
1394
2569
  }
1395
- const invoiceHash = decodeInvoice(payment.bolt11).paymentHash;
1396
- if (invoiceHash === null || !equalIgnoringCase(invoiceHash, payment.paymentHash)) {
1397
- return false;
2570
+ /**
2571
+ * Mints a socket ticket for anybody who asks, which makes the trigger's whole
2572
+ * stream public, preimages included. Only for a board where that is the point
2573
+ */
2574
+ publicWatchTicket(config) {
2575
+ return publicWatchTicketEndpoint(this.gateway, config);
1398
2576
  }
1399
- return preimageMatchesHash(payment.preimage, invoiceHash);
1400
- }
1401
- function wrapFeeCeiling(amountMsat, allowance) {
1402
- const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
1403
- return Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional));
1404
- }
1405
- function proveWrapped(wrapped, recipient, allowance) {
1406
- const offered = decodeInvoice(wrapped);
1407
- const owed = decodeInvoice(recipient);
1408
- const refuse = (code, detail) => {
1409
- throw new WrapRefusedError(code, detail);
1410
- };
1411
- if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
1412
- refuse("undecodable", "the wrapped invoice carries no hash, amount or expiry");
2577
+ /**
2578
+ * A verify endpoint of your own that asks the recipient's wallet for you, so
2579
+ * the gateway polls you and never the wallet
2580
+ */
2581
+ verify(config) {
2582
+ return lightningVerifyEndpoint(config);
1413
2583
  }
1414
- if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
1415
- refuse("undecodable", "the recipient's invoice carries no hash, amount or expiry");
2584
+ /** The verify endpoint a bank rail is polled at, answering off your own statement */
2585
+ bankVerify(config) {
2586
+ return bankVerifyEndpoint(config);
1416
2587
  }
1417
- if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
1418
- refuse("hash_mismatch", "settling the wrap would not settle the recipient");
2588
+ /**
2589
+ * The whole webhook route: it answers the gateway's challenge, checks the
2590
+ * signature against the key the gateway publishes, refuses a settlement that
2591
+ * proves nothing, and calls you for the one that does.
2592
+ *
2593
+ * `export const POST = gateway.serve.webhook({ onSettled: fulfil })` is the
2594
+ * entire integration
2595
+ */
2596
+ webhook(handlers) {
2597
+ const options = { toleranceSecs: handlers.toleranceSecs };
2598
+ return async (request) => {
2599
+ const credential = await this.credential(handlers);
2600
+ const challenge = await answerWebhookChallenge(request, credential, options);
2601
+ if (challenge !== null) {
2602
+ return challenge;
2603
+ }
2604
+ const settlement = await readSettlement(request, credential, options);
2605
+ if (settlement !== null) {
2606
+ return await acted(settlement, handlers);
2607
+ }
2608
+ const payment = await readPayment(request, credential, options);
2609
+ if (payment !== null) {
2610
+ await handlers.onPayment?.(payment);
2611
+ return new Response("ok");
2612
+ }
2613
+ return new Response("bad signature", { status: 401 });
2614
+ };
1419
2615
  }
1420
- const asked = offered.amountMsat ?? 0;
1421
- const due = owed.amountMsat ?? 0;
1422
- if (asked < due) {
1423
- refuse("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
2616
+ /** Verify a delivery and read the settlement out of it, null when it is not believable */
2617
+ async readSettlement(request, options) {
2618
+ return await readSettlement(request, await this.credential({}), options);
1424
2619
  }
1425
- const ceiling = wrapFeeCeiling(due, allowance);
1426
- if (asked - due > ceiling) {
1427
- refuse("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
2620
+ /** Verify a delivery and read the payment out of it, null when it is not believable */
2621
+ async readPayment(request, options) {
2622
+ return await readPayment(request, await this.credential({}), options);
1428
2623
  }
1429
- if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
1430
- refuse("recipient_expires_first", "the operator could be paid and find it cannot deliver");
2624
+ /** Answer the challenge the gateway sends before it will post to a webhook of yours */
2625
+ async answerWebhookChallenge(request, options) {
2626
+ return await answerWebhookChallenge(request, await this.credential({}), options);
1431
2627
  }
1432
- }
1433
- async function payRequestFor(listed, payment) {
1434
- const at = listed.indexOf("@");
1435
- const name = listed.slice(0, at);
1436
- const domain = listed.slice(at + 1).toLowerCase();
1437
- if (at < 1 || !publicHttps(`https://${domain}/`)) {
1438
- throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "not a usable address");
2628
+ async credential(handlers) {
2629
+ return handlers.credential ?? { publicKey: await this.gateway.webhookKey() };
1439
2630
  }
1440
- return reachable(`https://${domain}/.well-known/lnurlp/${name}`, payment);
1441
- }
1442
- async function reachable(url, payment) {
1443
- try {
1444
- if (!publicHttps(url)) {
1445
- throw new Error(`${url} is not a public https URL`);
1446
- }
1447
- const response = await fetch(url, {
1448
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
1449
- headers: { accept: "application/json" }
1450
- });
1451
- if (!response.ok) {
1452
- throw new Error(`answered ${response.status}`);
1453
- }
1454
- const body = await response.json();
1455
- if (body === null || typeof body !== "object") {
1456
- throw new Error("answered with no JSON object");
1457
- }
1458
- return body;
1459
- } catch (cause) {
1460
- throw new UnverifiedRecipientError(payment.lnAddress, payment.id, cause);
2631
+ };
2632
+ async function acted(settlement, handlers) {
2633
+ if (carriesProof(settlement)) {
2634
+ await handlers.onSettled?.(settlement);
2635
+ return new Response("ok");
1461
2636
  }
1462
- }
1463
- function equalIgnoringCase(one, other) {
1464
- return one.toLowerCase() === other.toLowerCase();
2637
+ if (handlers.onUnproven === void 0) {
2638
+ return new Response("the recipient released no preimage", { status: 202 });
2639
+ }
2640
+ await handlers.onUnproven(settlement);
2641
+ return new Response("ok");
1465
2642
  }
1466
2643
 
1467
2644
  // src/client.ts
@@ -1483,35 +2660,62 @@ function backoffMs(firstDelay, attempt) {
1483
2660
  const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
1484
2661
  return Math.round(grown * (0.5 + Math.random() / 2));
1485
2662
  }
2663
+ function askFromWire(raw) {
2664
+ let said;
2665
+ try {
2666
+ said = JSON.parse(raw);
2667
+ } catch {
2668
+ return null;
2669
+ }
2670
+ if (typeof said !== "object" || said === null) {
2671
+ return null;
2672
+ }
2673
+ const fields = said;
2674
+ const ask2 = fields["ask"];
2675
+ const paymentHash = fields["payment_hash"];
2676
+ return typeof ask2 === "string" && typeof paymentHash === "string" ? { ask: ask2, paymentHash } : null;
2677
+ }
2678
+ function gatewayAt(baseUrl) {
2679
+ const at = URL.canParse(baseUrl) ? new URL(baseUrl) : null;
2680
+ if (at === null || at.protocol !== "http:" && at.protocol !== "https:") {
2681
+ throw new Error(`${baseUrl} is not an http or https gateway url`);
2682
+ }
2683
+ return baseUrl.replace(/\/+$/, "");
2684
+ }
1486
2685
  var ThunderBridge = class {
1487
2686
  baseUrl;
1488
- verify;
1489
2687
  token;
2688
+ handedOut = /* @__PURE__ */ new Map();
1490
2689
  secret;
1491
2690
  strangers = null;
1492
2691
  speaks = null;
2692
+ published = null;
2693
+ /** Everything this gateway lets you mount, from an LNURL endpoint to a webhook route */
2694
+ serve;
2695
+ /** One call per sale, whatever the rail moves */
2696
+ rails;
1493
2697
  constructor(baseUrl, options) {
1494
- this.baseUrl = baseUrl.replace(/\/+$/, "");
1495
- this.verify = options?.verify ?? true;
2698
+ this.baseUrl = gatewayAt(baseUrl);
1496
2699
  this.token = options?.token ?? null;
1497
2700
  this.secret = options?.secret ?? null;
2701
+ this.serve = new Serve(this);
2702
+ this.rails = new Rails(this);
1498
2703
  }
1499
2704
  /**
1500
- * Whether a token was given, which is what makes an instance yours: a gateway
1501
- * started with `GATEWAY_TOKEN` answers nobody else, so anything you hand it
1502
- * stays between you and it
2705
+ * Whether a token was given to this instance, which is your side of the
2706
+ * arrangement and says nothing about the gateway's. `refusesStrangers` is the
2707
+ * one that asks the gateway, and it is the one to guard anything with
1503
2708
  */
1504
- get isPrivate() {
2709
+ get hasToken() {
1505
2710
  return this.token !== null;
1506
2711
  }
1507
2712
  /**
1508
2713
  * Whether the gateway turns away a caller carrying no token, asked by making
1509
- * one unauthenticated read it would have to refuse. `isPrivate` answers only
1510
- * whether you configured a token, which is your side of the arrangement and
1511
- * says nothing about the gateway's, so a made-up token against a public
1512
- * instance reads as private and is not. Asked once and remembered, because an
1513
- * instance does not change its mind. Anything other than a refusal counts as
1514
- * open, so an unreachable gateway fails closed
2714
+ * one unauthenticated read it would have to refuse. `hasToken` answers only
2715
+ * whether you configured one, so a made-up token against a public instance
2716
+ * reads as yours and is not. Asked once and remembered, because an instance
2717
+ * does not change its mind. Anything other than a refusal counts as open, so
2718
+ * an unreachable gateway fails closed
1515
2719
  */
1516
2720
  async refusesStrangers() {
1517
2721
  this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
@@ -1519,14 +2723,25 @@ var ThunderBridge = class {
1519
2723
  }).then((answer) => answer.status === 401).catch(() => false);
1520
2724
  return await this.strangers;
1521
2725
  }
2726
+ /**
2727
+ * Ask to be paid for one thing. It mints the invoice, proves it came from the
2728
+ * address you asked for, draws the QR and hands back one object with a way to
2729
+ * wait for the money. This is `mint` plus the two things every caller does next
2730
+ */
2731
+ async requestPayment(asked) {
2732
+ return paymentRequestOf(this, await this.mint(asked, asked), asked);
2733
+ }
1522
2734
  /**
1523
2735
  * Ask the gateway for an invoice payable to the first address on your list
1524
2736
  * that can issue a provable one, throws `NoWalletAvailableError` when none can
1525
2737
  * and `GatewayCheatError` when what comes back is not what you asked for
1526
2738
  */
1527
- async createPayment(params, options) {
2739
+ async mint(charge, options) {
2740
+ const askedAt = Math.floor(Date.now() / 1e3);
2741
+ const asked = await priced(charge);
1528
2742
  const sent = createRequestBody(
1529
- params,
2743
+ asked,
2744
+ charge.webhookUrl,
1530
2745
  options?.trigger ? sha256Hex(unguessable(options.trigger)) : null,
1531
2746
  options?.replay
1532
2747
  );
@@ -1542,10 +2757,9 @@ var ThunderBridge = class {
1542
2757
  if (!response.ok) {
1543
2758
  throw await problemFrom(response);
1544
2759
  }
1545
- const payment = await paymentFrom(response);
1546
- if (this.verify) {
1547
- await proveOrigin(payment, params);
1548
- }
2760
+ const payment = await mintedFrom(response);
2761
+ await proveOrigin(payment, asked, askedAt);
2762
+ this.handOut(payment, options?.idempotencyKey ?? null);
1549
2763
  return payment;
1550
2764
  }
1551
2765
  /**
@@ -1555,8 +2769,9 @@ var ThunderBridge = class {
1555
2769
  * whether a wallet returns a provable invoice cannot be known without asking
1556
2770
  * it for one, and asking mints it
1557
2771
  */
1558
- async createQuote(params) {
1559
- const sent = quoteRequestBody(params);
2772
+ async quote(charge) {
2773
+ const asked = await priced(charge);
2774
+ const sent = quoteRequestBody(asked);
1560
2775
  const response = await fetch(`${this.baseUrl}/quotes`, {
1561
2776
  method: "POST",
1562
2777
  headers: await this.sending("/quotes", sent),
@@ -1565,22 +2780,31 @@ var ThunderBridge = class {
1565
2780
  if (!response.ok) {
1566
2781
  throw await problemFrom(response);
1567
2782
  }
1568
- const quote = quoteFromWire(await response.json().catch(() => null));
1569
- if (quote === null) {
2783
+ const quote2 = quoteFromWire(await response.json().catch(() => null));
2784
+ if (quote2 === null) {
1570
2785
  throw new ProblemError({
1571
2786
  status: response.status,
1572
2787
  title: "The gateway answered with something that is not a quote"
1573
2788
  });
1574
2789
  }
1575
- return quote;
2790
+ if (!asked.paidTo.some((address) => address.toLowerCase() === quote2.lnAddress.toLowerCase())) {
2791
+ throw new GatewayCheatError("address_not_requested", "");
2792
+ }
2793
+ return quote2;
1576
2794
  }
1577
- /** Read a payment back, null when the gateway has never heard of it */
1578
2795
  /**
1579
- * The key this gateway signs webhooks with when you registered none of your own,
1580
- * ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
1581
- * keep it: it is the same for every instance in the cluster
2796
+ * The key this gateway signs webhooks with when you registered none of your own.
2797
+ * `serve.webhook` reads it for you. Asked once and kept, because it is the same
2798
+ * for every instance in the cluster
1582
2799
  */
1583
2800
  async webhookKey() {
2801
+ this.published ??= this.publishedKey().catch((refused) => {
2802
+ this.published = null;
2803
+ throw refused;
2804
+ });
2805
+ return await this.published;
2806
+ }
2807
+ async publishedKey() {
1584
2808
  const response = await fetch(`${this.baseUrl}/webhook-key`);
1585
2809
  if (!response.ok) {
1586
2810
  throw await problemFrom(response);
@@ -1594,26 +2818,13 @@ var ThunderBridge = class {
1594
2818
  }
1595
2819
  return body.public_key;
1596
2820
  }
1597
- async getPayment(id) {
1598
- const path = `/incoming-payments/${encodeURIComponent(id)}`;
1599
- const response = await fetch(`${this.baseUrl}${path}`, {
1600
- headers: await this.reading("GET", path)
1601
- });
1602
- if (response.status === 404) {
1603
- return null;
1604
- }
1605
- if (!response.ok) {
1606
- throw await problemFrom(response);
1607
- }
1608
- return this.checked(await paymentFrom(response));
1609
- }
1610
2821
  /**
1611
- * Read back a payment the gateway is only watching, null when it has never
1612
- * heard of it. A watched payment carries no address, amount or invoice, so
1613
- * `getPayment` refuses it and this reads the shape both rails share
2822
+ * Read a payment back, null when the gateway has never heard of it. One method
2823
+ * for both sorts: `kind` says whether the gateway minted it or was handed it,
2824
+ * and the address, amount and invoice are null on one it was never told
1614
2825
  */
1615
- async getWatched(id) {
1616
- const path = `/incoming-payments/${encodeURIComponent(id)}`;
2826
+ async payment(held) {
2827
+ const path = `/incoming-payments/${encodeURIComponent(held.id)}`;
1617
2828
  const response = await fetch(`${this.baseUrl}${path}`, {
1618
2829
  headers: await this.reading("GET", path)
1619
2830
  });
@@ -1623,14 +2834,7 @@ var ThunderBridge = class {
1623
2834
  if (!response.ok) {
1624
2835
  throw await problemFrom(response);
1625
2836
  }
1626
- const watched = triggerEventFromWire(await response.json().catch(() => null));
1627
- if (watched === null) {
1628
- throw new ProblemError({
1629
- status: response.status,
1630
- title: "The gateway answered with something that is not a payment"
1631
- });
1632
- }
1633
- return this.proven(watched);
2837
+ return this.proven(await paymentFrom(response), held);
1634
2838
  }
1635
2839
  /**
1636
2840
  * List what this gateway is watching, newest first. Only a gateway started
@@ -1641,7 +2845,7 @@ var ThunderBridge = class {
1641
2845
  * Anything older than that window is not in the answer, and the list does not
1642
2846
  * pretend otherwise
1643
2847
  */
1644
- async listPayments(limit) {
2848
+ async payments(limit) {
1645
2849
  const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
1646
2850
  const response = await fetch(`${this.baseUrl}${path}`, {
1647
2851
  headers: await this.reading("GET", path)
@@ -1650,14 +2854,14 @@ var ThunderBridge = class {
1650
2854
  throw await problemFrom(response);
1651
2855
  }
1652
2856
  const body = await response.json().catch(() => null);
1653
- const listed = Array.isArray(body?.payments) ? body.payments.map(triggerEventFromWire).filter((one) => one !== null) : null;
1654
- if (listed === null || typeof body?.settled_scanned !== "number") {
2857
+ const listed2 = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null).filter((one) => one.status !== "paid" || carriesProof(one)) : null;
2858
+ if (listed2 === null || typeof body?.settled_scanned !== "number") {
1655
2859
  throw new ProblemError({
1656
2860
  status: response.status,
1657
2861
  title: "The gateway answered with something that is not a payment list"
1658
2862
  });
1659
2863
  }
1660
- return { payments: listed, scanned: body.settled_scanned };
2864
+ return { payments: listed2, scanned: body.settled_scanned };
1661
2865
  }
1662
2866
  /**
1663
2867
  * Follow a payment over WebSocket until it is paid or expired, reconnecting
@@ -1665,34 +2869,15 @@ var ThunderBridge = class {
1665
2869
  * one that has answered is followed until its own expiry, so the wait always
1666
2870
  * ends by itself
1667
2871
  */
1668
- async waitForPayment(id, options) {
1669
- const payment = paymentFromWire(await this.followed(id, options));
1670
- if (payment === null) {
1671
- throw new ProblemError({
1672
- status: 200,
1673
- title: `payment ${id} is watched rather than minted here, read it with waitForWatched`
1674
- });
1675
- }
1676
- return this.checked(payment);
1677
- }
1678
- /**
1679
- * Follow a payment the gateway is only watching, one it did not mint, until it
1680
- * is paid or expired.
1681
- *
1682
- * A watched payment carries no address, no amount and no invoice, because the
1683
- * gateway was told none of them, so it reads back as the shape a trigger
1684
- * streams rather than as a `Payment`. That is every bank transfer, and every
1685
- * Lightning invoice registered with `watchPayment` instead of `createPayment`.
1686
- */
1687
- async waitForWatched(id, options) {
1688
- const watched = triggerEventFromWire(await this.followed(id, options));
1689
- if (watched === null) {
2872
+ async settled(held, options) {
2873
+ const ended = paymentFromWire(await this.followed(held.id, options));
2874
+ if (ended === null) {
1690
2875
  throw new ProblemError({
1691
2876
  status: 200,
1692
2877
  title: "The gateway answered with something that is not a payment"
1693
2878
  });
1694
2879
  }
1695
- return this.proven(watched);
2880
+ return this.proven(ended, held);
1696
2881
  }
1697
2882
  followed(id, options) {
1698
2883
  const base = this.baseUrl.replace(/^http/, "ws");
@@ -1759,7 +2944,7 @@ var ThunderBridge = class {
1759
2944
  opened.onmessage = (event) => {
1760
2945
  try {
1761
2946
  const frame = JSON.parse(String(event.data));
1762
- const watched = triggerEventFromWire(frame);
2947
+ const watched = paymentFromWire(frame);
1763
2948
  if (watched === null) {
1764
2949
  return;
1765
2950
  }
@@ -1788,17 +2973,17 @@ var ThunderBridge = class {
1788
2973
  * what you want is the one that arrives and nothing further from the other.
1789
2974
  *
1790
2975
  * A leg that expires is a loser, not a winner, which is the whole reason this is
1791
- * not a race: `waitForPayment` ends on `paid` and on `expired` alike, and a
2976
+ * not a race: `settled` ends on `paid` and on `expired` alike, and a
1792
2977
  * Lightning invoice expires in an hour while a bank transfer takes days. `null`
1793
2978
  * means every leg ended without being paid.
1794
2979
  *
1795
2980
  * Stopping the wait is not revoking the invoice. Nobody can revoke one, because
1796
2981
  * the recipient's own wallet minted it, so a payer who pays the loser afterwards
1797
- * really does pay twice and that shows up on `followTrigger` as a second
1798
- * settlement to refund.
2982
+ * really does pay twice and that shows up on `follow` as a second settlement to
2983
+ * refund.
1799
2984
  */
1800
- async firstToSettle(ids, options) {
1801
- if (ids.length === 0) {
2985
+ async firstSettled(held, options) {
2986
+ if (held.length === 0) {
1802
2987
  return null;
1803
2988
  }
1804
2989
  const stopLosers = new AbortController();
@@ -1806,15 +2991,15 @@ var ThunderBridge = class {
1806
2991
  let refused = null;
1807
2992
  try {
1808
2993
  const winner = await new Promise((resolve2) => {
1809
- let waiting = ids.length;
2994
+ let waiting = held.length;
1810
2995
  const lost = () => {
1811
2996
  waiting -= 1;
1812
2997
  if (waiting === 0) {
1813
2998
  resolve2(null);
1814
2999
  }
1815
3000
  };
1816
- for (const id of ids) {
1817
- this.waitForWatched(id, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
3001
+ for (const leg of held) {
3002
+ this.settled(leg, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
1818
3003
  refused ??= failure;
1819
3004
  lost();
1820
3005
  });
@@ -1834,10 +3019,10 @@ var ThunderBridge = class {
1834
3019
  * rather than one recipient, which is what makes leaving it cheap. Anything
1835
3020
  * the watcher needs goes in `sealed`, which the gateway cannot read
1836
3021
  */
1837
- async watchPayment(params) {
3022
+ async watch(handover) {
1838
3023
  const sent = watchRequestBody(
1839
- params,
1840
- params.trigger ? sha256Hex(unguessable(params.trigger)) : null
3024
+ handover,
3025
+ handover.trigger ? sha256Hex(unguessable(handover.trigger)) : null
1841
3026
  );
1842
3027
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
1843
3028
  method: "POST",
@@ -1847,17 +3032,20 @@ var ThunderBridge = class {
1847
3032
  if (!response.ok) {
1848
3033
  throw await problemFrom(response);
1849
3034
  }
1850
- const watched = triggerEventFromWire(await response.json().catch(() => null));
3035
+ const watched = paymentFromWire(await response.json().catch(() => null));
1851
3036
  if (watched === null) {
1852
3037
  throw new ProblemError({
1853
3038
  status: response.status,
1854
3039
  title: "The gateway answered with something that is not a watched payment"
1855
3040
  });
1856
3041
  }
1857
- const named = await this.nameFor(params.paymentHash);
3042
+ const named = await this.nameFor(handover.paymentHash);
1858
3043
  if (named !== null && watched.id !== named) {
1859
3044
  throw new GatewayCheatError("id_not_mine", watched.id);
1860
3045
  }
3046
+ if (watched.paymentHash.toLowerCase() !== handover.paymentHash.toLowerCase()) {
3047
+ throw new GatewayCheatError("hash_mismatch", watched.id);
3048
+ }
1861
3049
  return watched;
1862
3050
  }
1863
3051
  /**
@@ -1870,12 +3058,75 @@ var ThunderBridge = class {
1870
3058
  async nameFor(paymentHash) {
1871
3059
  return this.secret === null ? null : paymentNamedBy((await this.speaking()).publicKeyHex, paymentHash);
1872
3060
  }
3061
+ /**
3062
+ * Hold a socket open and answer what the gateway asks about this caller's own
3063
+ * payments, so a watch addressed to this caller settles without anybody
3064
+ * hosting a URL. Reconnects on its own until the returned function is called
3065
+ */
3066
+ attend(options) {
3067
+ const base = this.baseUrl.replace(/^http/, "ws");
3068
+ const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
3069
+ let socket = null;
3070
+ let retry;
3071
+ let stopped = false;
3072
+ let attempt = 0;
3073
+ const again = () => {
3074
+ if (stopped || options.reconnect === false) {
3075
+ return;
3076
+ }
3077
+ retry = setTimeout(() => void connect(), backoffMs(firstDelay, attempt));
3078
+ };
3079
+ const answered3 = async (opened, raw) => {
3080
+ const asked = askFromWire(raw);
3081
+ if (asked === null) {
3082
+ return;
3083
+ }
3084
+ const preimage = await options.answer(asked.paymentHash);
3085
+ opened.send(
3086
+ JSON.stringify(
3087
+ preimage === null ? { ask: asked.ask, settled: false } : { ask: asked.ask, settled: true, preimage }
3088
+ )
3089
+ );
3090
+ };
3091
+ const connect = async () => {
3092
+ attempt += 1;
3093
+ let url;
3094
+ try {
3095
+ url = `${base}/ws/tickets/${await this.wsTicket({ agent: true })}`;
3096
+ } catch (refused) {
3097
+ options.onError?.(refused);
3098
+ again();
3099
+ return;
3100
+ }
3101
+ if (stopped) {
3102
+ return;
3103
+ }
3104
+ const opened = new WebSocket(url);
3105
+ socket = opened;
3106
+ opened.onopen = () => {
3107
+ attempt = 1;
3108
+ };
3109
+ opened.onmessage = (event) => {
3110
+ void answered3(opened, String(event.data)).catch(
3111
+ (refused) => options.onError?.(refused)
3112
+ );
3113
+ };
3114
+ opened.onerror = () => options.onError?.(new Error(`could not attend ${base}`));
3115
+ opened.onclose = again;
3116
+ };
3117
+ void connect();
3118
+ return () => {
3119
+ stopped = true;
3120
+ clearTimeout(retry);
3121
+ socket?.close();
3122
+ };
3123
+ }
1873
3124
  /**
1874
3125
  * Follow every payment made to one trigger, replayed from the recent ones on
1875
3126
  * connect and then live, reconnecting on its own until the returned function
1876
3127
  * is called. A trigger has no terminal state, so this never resolves
1877
3128
  */
1878
- followTrigger(secret, options) {
3129
+ follow(secret, options) {
1879
3130
  const base = this.baseUrl.replace(/^http/, "ws");
1880
3131
  const asked = options.replay === void 0 ? "" : `?replay=${options.replay}`;
1881
3132
  const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}${asked}`;
@@ -1914,7 +3165,7 @@ var ThunderBridge = class {
1914
3165
  };
1915
3166
  socket.onmessage = (event) => {
1916
3167
  try {
1917
- const settled = triggerEventFromWire(JSON.parse(String(event.data)));
3168
+ const settled = paymentFromWire(JSON.parse(String(event.data)));
1918
3169
  if (settled !== null) {
1919
3170
  options.onPayment(this.proven(settled));
1920
3171
  }
@@ -1936,17 +3187,17 @@ var ThunderBridge = class {
1936
3187
  * A one minute pass onto one trigger's stream, for something that must hold
1937
3188
  * neither the token nor the trigger secret. Mint it in a handler and answer
1938
3189
  * with the ticket alone, because that is all a browser needs to connect and
1939
- * all it can do anything with. `watchTicketEndpoint` is this method already
3190
+ * all it can do anything with. `serve.watchTicket` is this method already
1940
3191
  * wrapped in a route
1941
3192
  */
1942
- async createSocketTicket(params) {
3193
+ async ticket(trigger, options) {
1943
3194
  return await this.mintedTicket({
1944
- trigger_secret: unguessable(params.trigger),
1945
- replay: params.replay
3195
+ trigger_secret: unguessable(trigger),
3196
+ replay: options?.replay
1946
3197
  });
1947
3198
  }
1948
3199
  needsTicket(asked) {
1949
- return asked === true || this.token !== null;
3200
+ return asked === true || this.token !== null || this.secret !== null;
1950
3201
  }
1951
3202
  async wsTicket(body) {
1952
3203
  return encodeURIComponent((await this.mintedTicket(body)).ticket);
@@ -1983,15 +3234,26 @@ var ThunderBridge = class {
1983
3234
  this.speaks ??= callerKey(this.secret ?? "");
1984
3235
  return await this.speaks;
1985
3236
  }
1986
- proven(settled) {
1987
- const unproven = settled.status === "paid" && (settled.preimage === null || !preimageMatchesHash(settled.preimage, settled.paymentHash));
1988
- if (this.verify && unproven) {
1989
- throw new GatewayCheatError("preimage_mismatch", settled.id);
3237
+ handOut(payment, key) {
3238
+ const now = Math.floor(Date.now() / 1e3);
3239
+ for (const [hash2, earlier2] of this.handedOut) {
3240
+ if (earlier2.expiresAt <= now) {
3241
+ this.handedOut.delete(hash2);
3242
+ }
3243
+ }
3244
+ const hash = payment.paymentHash.toLowerCase();
3245
+ const earlier = this.handedOut.get(hash);
3246
+ if (earlier !== void 0 && (earlier.key === null || earlier.key !== key)) {
3247
+ throw new GatewayCheatError("invoice_reused", payment.id);
1990
3248
  }
1991
- return settled;
3249
+ this.handedOut.set(hash, { key, expiresAt: payment.expiresAt });
1992
3250
  }
1993
- checked(payment) {
1994
- if (this.verify && payment.status === "paid" && !isProvablyPaid(payment)) {
3251
+ proven(payment, held) {
3252
+ const other = held !== void 0 && (payment.id !== held.id || payment.paymentHash.toLowerCase() !== held.paymentHash.toLowerCase());
3253
+ if (other) {
3254
+ throw new GatewayCheatError("hash_mismatch", payment.id);
3255
+ }
3256
+ if (payment.status === "paid" && !carriesProof(payment)) {
1995
3257
  throw new GatewayCheatError("preimage_mismatch", payment.id);
1996
3258
  }
1997
3259
  return payment;
@@ -2007,11 +3269,21 @@ async function paymentFrom(response) {
2007
3269
  }
2008
3270
  return payment;
2009
3271
  }
3272
+ async function mintedFrom(response) {
3273
+ const payment = mintedFromWire(await response.json().catch(() => null));
3274
+ if (payment === null) {
3275
+ throw new ProblemError({
3276
+ status: response.status,
3277
+ title: "The gateway answered with something that is not a minted payment"
3278
+ });
3279
+ }
3280
+ return payment;
3281
+ }
2010
3282
  async function problemFrom(response) {
2011
3283
  const problem = await response.json().catch(() => ({}));
2012
3284
  const document = { ...problem, status: response.status };
2013
3285
  if (isProblemType(problem, NO_WALLET_AVAILABLE)) {
2014
- return new NoWalletAvailableError(document, refusals(problem.wallets));
3286
+ return new NoWalletAvailableError(document, refusals2(problem.wallets));
2015
3287
  }
2016
3288
  if (isProblemType(problem, REQUEST_IN_FLIGHT)) {
2017
3289
  return new IdempotencyConflictError(document, "request-in-flight");
@@ -2021,7 +3293,7 @@ async function problemFrom(response) {
2021
3293
  }
2022
3294
  return new ProblemError(document);
2023
3295
  }
2024
- function refusals(wallets) {
3296
+ function refusals2(wallets) {
2025
3297
  if (!Array.isArray(wallets)) {
2026
3298
  return [];
2027
3299
  }
@@ -2030,118 +3302,6 @@ function refusals(wallets) {
2030
3302
  });
2031
3303
  }
2032
3304
 
2033
- // src/fio.ts
2034
- var BASE_URL = "https://fioapi.fio.cz/v1/rest";
2035
- var MINOR_UNITS = 100;
2036
- var MILLIS = 1e3;
2037
- var DATE_CHARS = "2026-08-04".length;
2038
- var DEFAULT_MIN_INTERVAL_SECS = 30;
2039
- var TOO_SOON = 409;
2040
- var BOOKED_AT = "column0";
2041
- var AMOUNT = "column1";
2042
- var CURRENCY = "column14";
2043
- var VARIABLE_SYMBOL = "column5";
2044
- var USER_IDENTIFICATION = "column7";
2045
- var MESSAGE_FOR_RECIPIENT = "column16";
2046
- var PAYER_REFERENCE = "column27";
2047
- var WHERE_A_PAYER_WRITES = [
2048
- MESSAGE_FOR_RECIPIENT,
2049
- VARIABLE_SYMBOL,
2050
- USER_IDENTIFICATION,
2051
- PAYER_REFERENCE
2052
- ];
2053
- var UTC_OFFSET = /^[+-]\d{4}$/;
2054
- function fioStatement(config) {
2055
- const named = typeof config.token === "string" ? [config.token] : config.token;
2056
- if (named.length === 0) {
2057
- throw new Error("fioStatement needs at least one token to read with");
2058
- }
2059
- const usedAt = new Map(named.map((token) => [token, Number.NEGATIVE_INFINITY]));
2060
- const tokenWindowMs = (config.minIntervalSecs ?? DEFAULT_MIN_INTERVAL_SECS) * MILLIS;
2061
- const paceMs = tokenWindowMs / usedAt.size;
2062
- let lastRead = Number.NEGATIVE_INFINITY;
2063
- let credits = [];
2064
- return async (sinceUnix) => {
2065
- const now = Date.now();
2066
- if (now - lastRead < paceMs) {
2067
- return credits;
2068
- }
2069
- const idlest = longestUnused(usedAt);
2070
- if (now - idlest.usedAt < tokenWindowMs) {
2071
- return credits;
2072
- }
2073
- usedAt.set(idlest.token, now);
2074
- lastRead = now;
2075
- const url = [
2076
- config.baseUrl ?? BASE_URL,
2077
- "periods",
2078
- idlest.token,
2079
- asDate(sinceUnix),
2080
- asDate(Math.floor(now / MILLIS)),
2081
- "transactions.json"
2082
- ].join("/");
2083
- const answer = await fetch(url, { headers: { accept: "application/json" } });
2084
- if (answer.status === TOO_SOON) {
2085
- throw new Error("fio refuses a second read of this token inside its 30 second window");
2086
- }
2087
- if (!answer.ok) {
2088
- throw new Error(`fio answered ${answer.status} reading the statement`);
2089
- }
2090
- const read = await answer.json();
2091
- credits = (read.accountStatement?.transactionList?.transaction ?? []).filter(isCredit).map(asCredit);
2092
- return credits;
2093
- };
2094
- }
2095
- function longestUnused(usedAt) {
2096
- let token = "";
2097
- let idleSince = Number.POSITIVE_INFINITY;
2098
- for (const [candidate, at] of usedAt) {
2099
- if (at < idleSince) {
2100
- token = candidate;
2101
- idleSince = at;
2102
- }
2103
- }
2104
- return { token, usedAt: idleSince };
2105
- }
2106
- function isCredit(transaction) {
2107
- return numberIn(transaction[AMOUNT]) > 0;
2108
- }
2109
- function asCredit(transaction) {
2110
- return {
2111
- amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
2112
- currency: textIn(transaction[CURRENCY]),
2113
- reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
2114
- bookedAt: bookedAtIn(transaction[BOOKED_AT])
2115
- };
2116
- }
2117
- function bookedAtIn(cell) {
2118
- const value = cell?.value;
2119
- if (typeof value === "number") {
2120
- return Math.floor(value / MILLIS);
2121
- }
2122
- const at = Date.parse(asIso8601(textIn(cell)));
2123
- return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
2124
- }
2125
- function asIso8601(booked) {
2126
- const day = booked.slice(0, DATE_CHARS);
2127
- const zone = booked.slice(DATE_CHARS);
2128
- if (!UTC_OFFSET.test(zone)) {
2129
- return `${day}T00:00:00Z`;
2130
- }
2131
- return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
2132
- }
2133
- function numberIn(cell) {
2134
- const value = Number(cell?.value ?? Number.NaN);
2135
- return Number.isFinite(value) ? value : 0;
2136
- }
2137
- function textIn(cell) {
2138
- const value = cell?.value;
2139
- return value === null || value === void 0 ? "" : String(value).trim();
2140
- }
2141
- function asDate(unix) {
2142
- return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
2143
- }
2144
-
2145
3305
  // src/gateways.ts
2146
3306
  var Gateways = class {
2147
3307
  each;
@@ -2165,10 +3325,8 @@ var Gateways = class {
2165
3325
  * carrying the first refusal, because one gateway that agreed is enough to be
2166
3326
  * watched
2167
3327
  */
2168
- async watchPayment(params) {
2169
- const asked = await Promise.allSettled(
2170
- this.each.map((gateway) => gateway.watchPayment(params))
2171
- );
3328
+ async watch(handover) {
3329
+ const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watch(handover)));
2172
3330
  const taken = [];
2173
3331
  const refusals3 = [];
2174
3332
  for (const [at, answer] of asked.entries()) {
@@ -2190,7 +3348,7 @@ var Gateways = class {
2190
3348
  * saying so. When they all end without a payment, the first ending is the answer,
2191
3349
  * and when they all fail, the first failure is thrown
2192
3350
  */
2193
- async waitForWatched(id, options) {
3351
+ async settled(held, options) {
2194
3352
  const stopLosers = new AbortController();
2195
3353
  const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
2196
3354
  const unpaid = [];
@@ -2205,7 +3363,7 @@ var Gateways = class {
2205
3363
  }
2206
3364
  };
2207
3365
  for (const gateway of this.each) {
2208
- gateway.waitForWatched(id, { ...options, signal }).then((watched) => {
3366
+ gateway.settled(held, { ...options, signal }).then((watched) => {
2209
3367
  if (watched.status === "paid") {
2210
3368
  return resolve2(watched);
2211
3369
  }
@@ -2229,315 +3387,30 @@ var Gateways = class {
2229
3387
  }
2230
3388
  }
2231
3389
  };
2232
-
2233
- // src/price.ts
2234
- var MSAT_PER_BTC2 = 1e11;
2235
- var BASIS_POINTS = 1e4;
2236
- var DEFAULT_HOLD_SECS = 60;
2237
- var DEFAULT_MIN_VENUES = 2;
2238
- var DEFAULT_MAX_SPREAD_BPS = 200;
2239
- var MILLIS2 = 1e3;
2240
- function coinbase(baseUrl = "https://api.coinbase.com") {
2241
- return async (currency) => {
2242
- const asked = currency.toUpperCase();
2243
- const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
2244
- const quoted = answer.data;
2245
- if (quoted?.currency !== asked) {
2246
- throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
2247
- }
2248
- return asMinor(quoted.amount, "coinbase", asked);
2249
- };
2250
- }
2251
- function kraken(baseUrl = "https://api.kraken.com") {
2252
- return async (currency) => {
2253
- const asked = currency.toUpperCase();
2254
- const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
2255
- if (answer.error?.length) {
2256
- throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
2257
- }
2258
- const pairs = Object.values(answer.result ?? {});
2259
- if (pairs.length !== 1) {
2260
- throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
2261
- }
2262
- return asMinor(pairs[0]?.c?.[0], "kraken", asked);
2263
- };
2264
- }
2265
- function bitstamp(baseUrl = "https://www.bitstamp.net") {
2266
- return async (currency) => {
2267
- const pair = `btc${currency.toLowerCase()}`;
2268
- const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
2269
- if (Array.isArray(answer)) {
2270
- throw new Error(`bitstamp has no ${pair} pair`);
2271
- }
2272
- return asMinor(answer.last, "bitstamp", currency);
2273
- };
2274
- }
2275
- function coinmate(baseUrl = "https://coinmate.io") {
2276
- return async (currency) => {
2277
- const pair = `BTC_${currency.toUpperCase()}`;
2278
- const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
2279
- if (answer.error !== false) {
2280
- throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
2281
- }
2282
- return asMinor(answer.data?.last, "coinmate", currency);
2283
- };
2284
- }
2285
- function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
2286
- const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS2;
2287
- const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
2288
- const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
2289
- const held = /* @__PURE__ */ new Map();
2290
- return async (currency) => {
2291
- const asked = currency.toUpperCase();
2292
- const remembered = held.get(asked);
2293
- if (remembered && Date.now() - remembered.at < holdFor) {
2294
- return remembered.price;
2295
- }
2296
- const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
2297
- const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
2298
- if (quoted.length < minVenues) {
2299
- throw new Error(
2300
- `${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals2(answers)}`
2301
- );
2302
- }
2303
- const spread = spreadBpsOf(quoted);
2304
- if (spread > maxSpread) {
2305
- throw new Error(
2306
- `venues disagree on ${asked} by ${Math.round(spread)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
2307
- );
2308
- }
2309
- const price = middleOf(quoted);
2310
- held.set(asked, { at: Date.now(), price });
2311
- return price;
2312
- };
2313
- }
2314
- function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
2315
- if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
2316
- throw new Error("amountMinor must be a whole number of minor units above zero");
2317
- }
2318
- if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
2319
- throw new Error(`${priceMinorPerBtc} is not a usable price`);
2320
- }
2321
- const spreadBps = options.spreadBps ?? 0;
2322
- if (!Number.isInteger(spreadBps) || spreadBps < 0) {
2323
- throw new Error("spreadBps must be a whole number of basis points, none or more");
2324
- }
2325
- const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
2326
- const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
2327
- const msat = (wanted + per - 1n) / per;
2328
- if (msat > BigInt(Number.MAX_SAFE_INTEGER)) {
2329
- throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
2330
- }
2331
- return Number(msat);
2332
- }
2333
- async function asJson(url, venue) {
2334
- const answer = await fetch(url, { headers: { accept: "application/json" } });
2335
- if (!answer.ok) {
2336
- throw new Error(`${venue} answered ${answer.status}`);
2337
- }
2338
- return answer.json();
2339
- }
2340
- function asMinor(quoted, venue, currency) {
2341
- const major2 = Number(quoted);
2342
- if (!Number.isFinite(major2) || major2 <= 0) {
2343
- throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
2344
- }
2345
- return Math.round(major2 * minorScaleOf(currency));
2346
- }
2347
- function spreadBpsOf(sorted) {
2348
- const lowest = sorted[0] ?? 0;
2349
- const highest = sorted[sorted.length - 1] ?? 0;
2350
- return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
2351
- }
2352
- function middleOf(sorted) {
2353
- const middle = Math.floor(sorted.length / 2);
2354
- if (sorted.length % 2 === 1) {
2355
- return sorted[middle] ?? 0;
2356
- }
2357
- return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
2358
- }
2359
- function refusals2(answers) {
2360
- return answers.filter((answer) => answer.status === "rejected").map(
2361
- (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
2362
- ).join("; ");
2363
- }
2364
-
2365
- // src/qr.ts
2366
- var import_uqr = require("uqr");
2367
- var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
2368
- function encodeForQr(destination) {
2369
- const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
2370
- return `LIGHTNING:${scanned}`;
2371
- }
2372
- function invoiceToSvg(destination, options) {
2373
- return svgOf(encodeForQr(destination), options);
2374
- }
2375
- function invoiceToDataUrl(destination, options) {
2376
- return asDataUrl(invoiceToSvg(destination, options));
2377
- }
2378
- function lnurlToSvg(endpoint, options) {
2379
- return svgOf(toLnurl(endpoint), options);
2380
- }
2381
- function lnurlToDataUrl(endpoint, options) {
2382
- return asDataUrl(lnurlToSvg(endpoint, options));
2383
- }
2384
- function spdToSvg(spd, options) {
2385
- return svgOf(spd, options);
2386
- }
2387
- function spdToDataUrl(spd, options) {
2388
- return asDataUrl(spdToSvg(spd, options));
2389
- }
2390
- function qrToSvg(payload, options) {
2391
- return svgOf(payload, options);
2392
- }
2393
- function qrToDataUrl(payload, options) {
2394
- return asDataUrl(qrToSvg(payload, options));
2395
- }
2396
- function svgOf(scanned, options) {
2397
- const size = options?.size ?? 256;
2398
- const color = options?.color ?? "#000";
2399
- if (!SAFE_COLOR.test(color)) {
2400
- throw new Error(`Invalid color: ${color}`);
2401
- }
2402
- return renderSvg(scanned, size, color);
2403
- }
2404
- function asDataUrl(svg) {
2405
- return `data:image/svg+xml,${encodeURIComponent(svg)}`;
2406
- }
2407
- function isLnAddress(destination) {
2408
- const at = destination.indexOf("@");
2409
- return at > 0 && destination.slice(at + 1).includes(".");
2410
- }
2411
- function renderSvg(content, size, color) {
2412
- const { data } = (0, import_uqr.encode)(content);
2413
- const modules = data.length;
2414
- const cellSize = size / modules;
2415
- let path = "";
2416
- for (let y = 0; y < modules; y++) {
2417
- for (let x = 0; x < modules; x++) {
2418
- if (data[y][x]) {
2419
- path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
2420
- }
2421
- }
2422
- }
2423
- return [
2424
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
2425
- `<rect width="100%" height="100%" fill="white"/>`,
2426
- `<path d="${path}" fill="${color}"/>`,
2427
- `</svg>`
2428
- ].join("");
2429
- }
2430
-
2431
- // src/rail.ts
2432
- var BANK = "bank";
2433
- var LIGHTNING = "lightning";
2434
- function bankRail(config) {
2435
- return async (order) => {
2436
- const expiresAt = config.expiresAt(order);
2437
- const transfer = await bankTransfer({
2438
- gateway: config.gateway,
2439
- secret: config.secret,
2440
- iban: config.iban,
2441
- verifyUrl: config.verifyUrl,
2442
- reference: order.reference,
2443
- amountMinor: order.amountMinor,
2444
- currency: order.currency,
2445
- expiresAt,
2446
- trigger: config.trigger,
2447
- replay: config.replay,
2448
- sealed: await config.sealed?.(order),
2449
- variableSymbol: config.variableSymbol?.(order),
2450
- webhookUrl: config.webhookUrl,
2451
- allowPublicGateway: config.allowPublicGateway
2452
- });
2453
- return {
2454
- id: transfer.id,
2455
- rail: config.name ?? BANK,
2456
- scan: transfer.spd,
2457
- qr: transfer.spd,
2458
- expiresAt
2459
- };
2460
- };
2461
- }
2462
- function lightningRail(config) {
2463
- return async (order) => {
2464
- const payment = await config.gateway.createPayment(
2465
- {
2466
- lnAddresses: config.lnAddresses,
2467
- amountMsat: await config.amountMsat(order),
2468
- webhookUrl: config.webhookUrl
2469
- },
2470
- {
2471
- idempotencyKey: config.idempotencyKey?.(order),
2472
- trigger: config.trigger,
2473
- replay: config.replay
2474
- }
2475
- );
2476
- return {
2477
- id: payment.id,
2478
- rail: config.name ?? LIGHTNING,
2479
- scan: payment.bolt11,
2480
- qr: encodeForQr(payment.bolt11),
2481
- expiresAt: payment.expiresAt
2482
- };
2483
- };
2484
- }
2485
3390
  // Annotate the CommonJS export names for ESM import in node:
2486
3391
  0 && (module.exports = {
3392
+ AmountError,
2487
3393
  GatewayCheatError,
2488
3394
  Gateways,
2489
- IDEMPOTENCY_KEY_REUSED,
2490
3395
  IdempotencyConflictError,
2491
- NO_WALLET_AVAILABLE,
2492
3396
  NoWalletAvailableError,
2493
- PAYMENT_ALREADY_WATCHED,
2494
3397
  ProblemError,
2495
- REQUEST_IN_FLIGHT,
2496
3398
  ThunderBridge,
2497
3399
  UnverifiedRecipientError,
2498
3400
  WrapRefusedError,
2499
3401
  answerVerifyChallenge,
2500
- answerVerifyChallengeRequest,
2501
- answerWebhookChallenge,
2502
- answerWebhookChallengeRequest,
2503
- bankRail,
2504
- bankTransfer,
2505
- bankVerifyEndpoint,
2506
- bitstamp,
2507
- coinbase,
2508
- coinmate,
3402
+ carriesProof,
2509
3403
  decodeInvoice,
2510
- fioStatement,
2511
- invoiceToDataUrl,
2512
- invoiceToSvg,
2513
- isProblemType,
2514
- isProvablyPaid,
2515
- isProvablySettled,
2516
- kraken,
2517
- lightningRail,
2518
- lnurlToDataUrl,
2519
- lnurlToSvg,
2520
- medianOf,
2521
- minorScaleOf,
2522
- minorUnitsOf,
2523
- msatFor,
2524
- parseSettlement,
2525
- parseSettlementRequest,
2526
- parseWatchedWebhook,
2527
- parseWatchedWebhookRequest,
2528
- parseWebhook,
2529
- parseWebhookRequest,
3404
+ fiat,
3405
+ invoiceFrom,
3406
+ msat,
2530
3407
  preimageMatchesHash,
2531
3408
  proveOrigin,
2532
3409
  proveSettlement,
2533
3410
  proveWrapped,
2534
- qrToDataUrl,
2535
- qrToSvg,
3411
+ relayedVerifyUrl,
3412
+ sats,
2536
3413
  seal,
2537
- spdToDataUrl,
2538
- spdToSvg,
2539
- toLnurl,
2540
3414
  unseal,
2541
- verifyWebhookSignature,
2542
3415
  wrapFeeCeiling
2543
3416
  });