thunder-bridge 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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(
@@ -377,158 +355,6 @@ function wordsToBytes(words) {
377
355
  return Uint8Array.from(bytes);
378
356
  }
379
357
 
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;
437
- }
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) {
456
- return false;
457
- }
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;
487
- }
488
- }
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;
530
- }
531
-
532
358
  // ../core/sealed.ts
533
359
  var VERSION = "v1";
534
360
  var IV_BYTES = 12;
@@ -594,12 +420,12 @@ function toBase64Url(bytes) {
594
420
  }
595
421
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
596
422
  }
597
- function fromBase64Url(text2) {
598
- if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) {
423
+ function fromBase64Url(text3) {
424
+ if (text3.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text3)) {
599
425
  return null;
600
426
  }
601
427
  try {
602
- const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
428
+ const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
603
429
  const bytes = new Uint8Array(binary.length);
604
430
  for (let at = 0; at < bytes.length; at++) {
605
431
  bytes[at] = binary.charCodeAt(at);
@@ -610,32 +436,109 @@ function fromBase64Url(text2) {
610
436
  }
611
437
  }
612
438
 
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;
439
+ // src/errors.ts
440
+ var AmountError = class extends Error {
441
+ /**
442
+ * Whether a failure is one of these, without asking whether it is this exact
443
+ * class. Every entry point carries its own copy of the class, so a price
444
+ * refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
445
+ * imported from `thunder-bridge`. The name and the code are the same in every
446
+ * copy, so this holds where `instanceof` does not
447
+ */
448
+ static is(failure) {
449
+ return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
629
450
  }
630
- let difference = 0;
631
- for (let at = 0; at < one.length; at++) {
632
- difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
451
+ code;
452
+ constructor(code, detail) {
453
+ super(`${code}: ${detail}`);
454
+ this.name = "AmountError";
455
+ this.code = code;
633
456
  }
634
- return difference === 0;
635
- }
636
- function asBuffer(bytes) {
637
- return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
457
+ };
458
+ var GatewayCheatError = class extends Error {
459
+ code;
460
+ paymentId;
461
+ constructor(code, paymentId) {
462
+ super(`gateway verification failed: ${code}`);
463
+ this.name = "GatewayCheatError";
464
+ this.code = code;
465
+ this.paymentId = paymentId;
466
+ }
467
+ };
468
+ var WrapRefusedError = class extends Error {
469
+ code;
470
+ constructor(code, detail) {
471
+ super(`wrapped invoice refused: ${code}, ${detail}`);
472
+ this.name = "WrapRefusedError";
473
+ this.code = code;
474
+ }
475
+ };
476
+ var UnverifiedRecipientError = class extends Error {
477
+ lnAddress;
478
+ paymentId;
479
+ constructor(lnAddress, paymentId, cause) {
480
+ super(`could not reach ${lnAddress} to verify the invoice: ${String(cause)}`);
481
+ this.name = "UnverifiedRecipientError";
482
+ this.lnAddress = lnAddress;
483
+ this.paymentId = paymentId;
484
+ }
485
+ };
486
+ var ProblemError = class extends Error {
487
+ static NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
488
+ static REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
489
+ static IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
490
+ static PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
491
+ static INVALID_REQUEST = "urn:problem-type:thunder-bridge:invalid-request";
492
+ static CALLER_UNKNOWN = "urn:problem-type:thunder-bridge:caller-unknown";
493
+ static VERIFY_HOST_REFUSED = "urn:problem-type:thunder-bridge:verify-host-refused";
494
+ static VERIFY_UNCONFIRMED = "urn:problem-type:thunder-bridge:verify-unconfirmed";
495
+ static VERIFY_UNCONSENTED = "urn:problem-type:thunder-bridge:verify-unconsented";
496
+ static WEBHOOK_UNCONFIRMED = "urn:problem-type:thunder-bridge:webhook-unconfirmed";
497
+ static TOO_MANY_PENDING = "urn:problem-type:thunder-bridge:too-many-pending";
498
+ /**
499
+ * Whether a problem carries this type. Branch on the type, never on the prose,
500
+ * and reach for `instanceof` first: every type this SDK gives a class to has one
501
+ */
502
+ static is(problem, type) {
503
+ return problem.type === type;
504
+ }
505
+ type;
506
+ title;
507
+ status;
508
+ detail;
509
+ constructor(problem) {
510
+ const title = problem.title ?? "Request failed";
511
+ super(problem.detail ? `${title}: ${problem.detail}` : title);
512
+ this.name = "ProblemError";
513
+ this.type = problem.type ?? "about:blank";
514
+ this.title = title;
515
+ this.status = problem.status ?? 0;
516
+ this.detail = problem.detail ?? null;
517
+ }
518
+ };
519
+ function isProblemType(problem, type) {
520
+ return problem.type === type;
638
521
  }
522
+ var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
523
+ var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
524
+ var IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
525
+ var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
526
+ var IdempotencyConflictError = class extends ProblemError {
527
+ conflict;
528
+ constructor(problem, conflict) {
529
+ super(problem);
530
+ this.name = "IdempotencyConflictError";
531
+ this.conflict = conflict;
532
+ }
533
+ };
534
+ var NoWalletAvailableError = class extends ProblemError {
535
+ wallets;
536
+ constructor(problem, wallets) {
537
+ super({ ...problem, type: NO_WALLET_AVAILABLE });
538
+ this.name = "NoWalletAvailableError";
539
+ this.wallets = wallets;
540
+ }
541
+ };
639
542
 
640
543
  // src/currency.ts
641
544
  var ISO_4217_MINOR_UNITS = {
@@ -669,72 +572,770 @@ var ISO_4217_MINOR_UNITS = {
669
572
  function minorUnitsOf(currency) {
670
573
  const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
671
574
  if (digits === void 0) {
672
- throw new Error(
575
+ throw new AmountError(
576
+ "unknown-currency",
673
577
  `${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
674
578
  );
675
579
  }
676
- return digits;
580
+ return digits;
581
+ }
582
+ function minorScaleOf(currency) {
583
+ return 10 ** minorUnitsOf(currency);
584
+ }
585
+
586
+ // src/price.ts
587
+ var MSAT_PER_BTC2 = 1e11;
588
+ var BASIS_POINTS = 1e4;
589
+ var DEFAULT_HOLD_SECS = 60;
590
+ var DEFAULT_MIN_VENUES = 2;
591
+ var DEFAULT_MAX_SPREAD_BPS = 200;
592
+ var MILLIS = 1e3;
593
+ function coinbase(baseUrl = "https://api.coinbase.com") {
594
+ return async (currency) => {
595
+ const asked = currency.toUpperCase();
596
+ const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
597
+ const quoted = answer.data;
598
+ if (quoted?.currency !== asked) {
599
+ throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
600
+ }
601
+ return asMinor(quoted.amount, "coinbase", asked);
602
+ };
603
+ }
604
+ function kraken(baseUrl = "https://api.kraken.com") {
605
+ return async (currency) => {
606
+ const asked = currency.toUpperCase();
607
+ const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
608
+ if (answer.error?.length) {
609
+ throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
610
+ }
611
+ const pairs = Object.values(answer.result ?? {});
612
+ if (pairs.length !== 1) {
613
+ throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
614
+ }
615
+ return asMinor(pairs[0]?.c?.[0], "kraken", asked);
616
+ };
617
+ }
618
+ function bitstamp(baseUrl = "https://www.bitstamp.net") {
619
+ return async (currency) => {
620
+ const pair = `btc${currency.toLowerCase()}`;
621
+ const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
622
+ if (Array.isArray(answer)) {
623
+ throw new Error(`bitstamp has no ${pair} pair`);
624
+ }
625
+ return asMinor(answer.last, "bitstamp", currency);
626
+ };
627
+ }
628
+ function coinmate(baseUrl = "https://coinmate.io") {
629
+ return async (currency) => {
630
+ const pair = `BTC_${currency.toUpperCase()}`;
631
+ const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
632
+ if (answer.error !== false) {
633
+ throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
634
+ }
635
+ return asMinor(answer.data?.last, "coinmate", currency);
636
+ };
637
+ }
638
+ function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
639
+ const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
640
+ const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
641
+ const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
642
+ const held = /* @__PURE__ */ new Map();
643
+ return async (currency) => {
644
+ const asked = currency.toUpperCase();
645
+ const remembered = held.get(asked);
646
+ if (remembered && Date.now() - remembered.at < holdFor) {
647
+ return remembered.price;
648
+ }
649
+ const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
650
+ const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
651
+ if (quoted.length < minVenues) {
652
+ throw new Error(
653
+ `${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
654
+ );
655
+ }
656
+ const spread2 = spreadBpsOf(quoted);
657
+ if (spread2 > maxSpread) {
658
+ throw new Error(
659
+ `venues disagree on ${asked} by ${Math.round(spread2)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
660
+ );
661
+ }
662
+ const price = middleOf(quoted);
663
+ held.set(asked, { at: Date.now(), price });
664
+ return price;
665
+ };
666
+ }
667
+ function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
668
+ if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
669
+ throw new Error("amountMinor must be a whole number of minor units above zero");
670
+ }
671
+ if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
672
+ throw new Error(`${priceMinorPerBtc} is not a usable price`);
673
+ }
674
+ const spreadBps = options.spreadBps ?? 0;
675
+ if (!Number.isInteger(spreadBps) || spreadBps < 0) {
676
+ throw new Error("spreadBps must be a whole number of basis points, none or more");
677
+ }
678
+ const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
679
+ const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
680
+ const msat2 = (wanted + per - 1n) / per;
681
+ if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
682
+ throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
683
+ }
684
+ return Number(msat2);
685
+ }
686
+ async function asJson(url, venue) {
687
+ const answer = await fetch(url, { headers: { accept: "application/json" } });
688
+ if (!answer.ok) {
689
+ throw new Error(`${venue} answered ${answer.status}`);
690
+ }
691
+ return answer.json();
692
+ }
693
+ function asMinor(quoted, venue, currency) {
694
+ const major2 = Number(quoted);
695
+ if (!Number.isFinite(major2) || major2 <= 0) {
696
+ throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
697
+ }
698
+ return Math.round(major2 * minorScaleOf(currency));
699
+ }
700
+ function spreadBpsOf(sorted) {
701
+ const lowest = sorted[0] ?? 0;
702
+ const highest = sorted[sorted.length - 1] ?? 0;
703
+ return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
704
+ }
705
+ function middleOf(sorted) {
706
+ const middle = Math.floor(sorted.length / 2);
707
+ if (sorted.length % 2 === 1) {
708
+ return sorted[middle] ?? 0;
709
+ }
710
+ return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
711
+ }
712
+ function refusals(answers) {
713
+ return answers.filter((answer) => answer.status === "rejected").map(
714
+ (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
715
+ ).join("; ");
716
+ }
717
+
718
+ // src/amount.ts
719
+ var MSAT_PER_SAT = 1e3;
720
+ var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
721
+ function sats(whole) {
722
+ if (!Number.isInteger(whole) || whole < 1 || whole > MAX_SATS) {
723
+ throw new AmountError("not-whole-satoshi", `${whole} is not a payable count of satoshi`);
724
+ }
725
+ return whole * MSAT_PER_SAT;
726
+ }
727
+ function msat(exact) {
728
+ if (!Number.isSafeInteger(exact) || exact < 1) {
729
+ throw new AmountError(
730
+ "not-whole-millisatoshi",
731
+ `${exact} is not a payable count of millisatoshi`
732
+ );
733
+ }
734
+ return exact;
735
+ }
736
+ function fiat(major2, currency, options) {
737
+ const amountMinor = minorFrom(major2, currency);
738
+ const rate = options?.rate ?? medianOf();
739
+ return async () => {
740
+ const wanted = msatFor(amountMinor, await rate(currency), { spreadBps: options?.spreadBps });
741
+ return sats(Math.ceil(wanted / MSAT_PER_SAT));
742
+ };
743
+ }
744
+ async function amountNow(amount) {
745
+ return typeof amount === "function" ? msat(await amount()) : msat(amount);
746
+ }
747
+ function minorFrom(major2, currency) {
748
+ const digits = minorUnitsOf(currency);
749
+ if (typeof major2 === "number") {
750
+ return payable(Math.round(major2 * minorScaleOf(currency)), major2, currency);
751
+ }
752
+ const parsed = /^(\d+)(?:[.,](\d+))?$/.exec(major2.trim());
753
+ if (parsed === null) {
754
+ throw new AmountError(
755
+ "not-a-decimal",
756
+ `${major2} is not a decimal ${currency.toUpperCase()} amount`
757
+ );
758
+ }
759
+ const fraction = parsed[2] ?? "";
760
+ if (fraction.length > digits) {
761
+ throw new AmountError(
762
+ "too-precise",
763
+ `${major2} carries more decimals than ${currency.toUpperCase()} has, which is ${digits}`
764
+ );
765
+ }
766
+ return payable(Number(`${parsed[1]}${fraction.padEnd(digits, "0")}`), major2, currency);
767
+ }
768
+ function payable(minor, major2, currency) {
769
+ if (!Number.isSafeInteger(minor) || minor < 1) {
770
+ throw new AmountError(
771
+ "not-a-decimal",
772
+ `${major2} is not a ${currency.toUpperCase()} price this can charge`
773
+ );
774
+ }
775
+ return minor;
776
+ }
777
+
778
+ // ../core/ed25519.ts
779
+ var ALGORITHM = { name: "Ed25519" };
780
+ var SEED_BYTES = 32;
781
+ var SIGNATURE_BYTES = 64;
782
+ var PUBLIC_KEY_BYTES = 32;
783
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
784
+ async function signingKeyFromSeed(seed) {
785
+ if (seed.length !== SEED_BYTES) {
786
+ throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
787
+ }
788
+ const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
789
+ wrapped.set(PKCS8_HEADER);
790
+ wrapped.set(seed, PKCS8_HEADER.length);
791
+ const key = await crypto.subtle.importKey("pkcs8", asBuffer(wrapped), ALGORITHM, true, ["sign"]);
792
+ const jwk = await crypto.subtle.exportKey("jwk", key);
793
+ return {
794
+ publicKeyHex: bytesToHex(fromBase64Url2(jwk.x ?? "")),
795
+ sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer(payload))))
796
+ };
797
+ }
798
+ async function verifyHex(publicKeyHex, signatureHex, payload) {
799
+ if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) {
800
+ return false;
801
+ }
802
+ if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) {
803
+ return false;
804
+ }
805
+ try {
806
+ const key = await crypto.subtle.importKey(
807
+ "raw",
808
+ asBuffer(hexToBytes(publicKeyHex)),
809
+ ALGORITHM,
810
+ false,
811
+ ["verify"]
812
+ );
813
+ return await crypto.subtle.verify(
814
+ ALGORITHM,
815
+ key,
816
+ asBuffer(hexToBytes(signatureHex)),
817
+ asBuffer(payload)
818
+ );
819
+ } catch {
820
+ return false;
821
+ }
822
+ }
823
+ function fromBase64Url2(text3) {
824
+ const padded = text3.replace(/-/g, "+").replace(/_/g, "/");
825
+ const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
826
+ const bytes = new Uint8Array(raw.length);
827
+ for (let at = 0; at < raw.length; at++) {
828
+ bytes[at] = raw.charCodeAt(at);
829
+ }
830
+ return bytes;
831
+ }
832
+ function asBuffer(bytes) {
833
+ return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
834
+ }
835
+
836
+ // ../core/hmac.ts
837
+ async function hmacHex(secret, payload) {
838
+ const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
839
+ const key = await crypto.subtle.importKey(
840
+ "raw",
841
+ asBuffer2(raw),
842
+ { name: "HMAC", hash: "SHA-256" },
843
+ false,
844
+ ["sign"]
845
+ );
846
+ const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
847
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
848
+ }
849
+ function equalInConstantTime(one, other) {
850
+ if (one.length !== other.length) {
851
+ return false;
852
+ }
853
+ let difference = 0;
854
+ for (let at = 0; at < one.length; at++) {
855
+ difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
856
+ }
857
+ return difference === 0;
858
+ }
859
+ function asBuffer2(bytes) {
860
+ return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
861
+ }
862
+
863
+ // ../core/caller.ts
864
+ var KEY_HEADER = "x-client-key";
865
+ var SIGNATURE_HEADER = "x-signature";
866
+ var TIMESTAMP_HEADER = "x-timestamp";
867
+ var PREFIX = "ed25519=";
868
+ var SIGNING_LABEL = "request-signing";
869
+ async function callerKey(secret) {
870
+ return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
871
+ }
872
+ function paymentNamedBy(caller, paymentHash) {
873
+ return sha256Hex(`${caller}.payment-id.${paymentHash}`);
874
+ }
875
+ async function signedAs(key, method, path, body) {
876
+ const timestamp = String(Math.floor(Date.now() / 1e3));
877
+ return {
878
+ [KEY_HEADER]: key.publicKeyHex,
879
+ [TIMESTAMP_HEADER]: timestamp,
880
+ [SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
881
+ };
882
+ }
883
+ function spoken(timestamp, method, path, body) {
884
+ return new TextEncoder().encode(
885
+ `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
886
+ );
887
+ }
888
+
889
+ // src/charge.ts
890
+ async function priced(charge) {
891
+ const paidTo = typeof charge.paidTo === "string" ? [charge.paidTo] : [...charge.paidTo];
892
+ if (paidTo.length === 0) {
893
+ throw new Error("name at least one lightning address to pay");
894
+ }
895
+ return { paidTo, amountMsat: await amountNow(charge.amount) };
896
+ }
897
+
898
+ // ../core/url.ts
899
+ var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
900
+ function publicHttps(raw) {
901
+ let url;
902
+ try {
903
+ url = new URL(raw);
904
+ } catch {
905
+ return false;
906
+ }
907
+ return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
908
+ }
909
+ function sameOrigin(one, other) {
910
+ try {
911
+ return new URL(one).origin === new URL(other).origin;
912
+ } catch {
913
+ return false;
914
+ }
915
+ }
916
+ function publicAddress(literal) {
917
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
918
+ }
919
+ function hostIsPublic(host) {
920
+ if (host.startsWith("[")) {
921
+ return publicAddress(host.slice(1, -1));
922
+ }
923
+ const name = host.replace(/\.$/, "");
924
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
925
+ return publicAddress(name);
926
+ }
927
+ const labels = name.split(".");
928
+ if (labels.length < 2 || labels.some((label) => label === "")) {
929
+ return false;
930
+ }
931
+ return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
932
+ }
933
+ function ipv4IsGlobal(literal) {
934
+ const parts = literal.split(".").map(Number);
935
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
936
+ return false;
937
+ }
938
+ const [a, b] = parts;
939
+ if (a === 0 || a === 10 || a === 127 || a >= 240) {
940
+ return false;
941
+ }
942
+ if (a === 100 && b >= 64 && b < 128) {
943
+ return false;
944
+ }
945
+ if (a === 169 && b === 254) {
946
+ return false;
947
+ }
948
+ if (a === 172 && b >= 16 && b < 32) {
949
+ return false;
950
+ }
951
+ if (a === 192 && b === 168) {
952
+ return false;
953
+ }
954
+ return true;
955
+ }
956
+ function ipv6IsGlobal(literal) {
957
+ const groups = ipv6Groups(literal.toLowerCase());
958
+ if (groups.length !== 8 || groups.some(Number.isNaN)) {
959
+ return false;
960
+ }
961
+ const leading = groups.slice(0, 5).every((group) => group === 0);
962
+ if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
963
+ return false;
964
+ }
965
+ if (leading && groups[5] === 65535) {
966
+ const [high, low] = [groups[6], groups[7]];
967
+ return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
968
+ }
969
+ const first = groups[0];
970
+ if ((first & 65024) === 64512) {
971
+ return false;
972
+ }
973
+ if ((first & 65472) === 65152) {
974
+ return false;
975
+ }
976
+ return true;
977
+ }
978
+ function ipv6Groups(address) {
979
+ const [head = "", tail] = address.split("::");
980
+ const left = head.split(":").filter((group) => group !== "");
981
+ const right = (tail ?? "").split(":").filter((group) => group !== "");
982
+ const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
983
+ return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
984
+ }
985
+
986
+ // ../core/outbound.ts
987
+ var HTTP_TIMEOUT_MS = 15e3;
988
+ var LOOKUP_TIMEOUT_MS = 5e3;
989
+ var HOPS_FOLLOWED = 2;
990
+ var REDIRECTS = [301, 302, 303, 307, 308];
991
+ var KEEPS_THE_METHOD = [307, 308];
992
+ var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
993
+ var BODY_LIMIT_BYTES = 262144;
994
+ async function ask(send, url, sent = {}) {
995
+ const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
996
+ const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
997
+ let target = url;
998
+ let carried = sent;
999
+ for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
1000
+ const at = await addressesToReach(target);
1001
+ const response = await send(target, carried, signal, at);
1002
+ if (!REDIRECTS.includes(response.status)) {
1003
+ return await answerOf(response);
1004
+ }
1005
+ const location = response.headers.get("location");
1006
+ if (location === null) {
1007
+ throw new Error(`${target} answered ${response.status} without saying where to`);
1008
+ }
1009
+ await response.body?.cancel();
1010
+ const next = new URL(location, target).toString();
1011
+ carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
1012
+ carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
1013
+ target = next;
1014
+ }
1015
+ throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
1016
+ }
1017
+ async function addressesToReach(url) {
1018
+ if (!publicHttps(url)) {
1019
+ throw new Error(`${url} is not a public https URL`);
1020
+ }
1021
+ const found = await resolved(url);
1022
+ if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
1023
+ throw new Error(`${url} resolves to an address we do not reach`);
1024
+ }
1025
+ return found;
1026
+ }
1027
+ async function resolved(url) {
1028
+ const { lookup } = await import("dns/promises");
1029
+ const { setTimeout: sleep } = await import("timers/promises");
1030
+ const host = new URL(url).hostname.replace(/^\[|]$/g, "");
1031
+ const found = await Promise.race([
1032
+ lookup(host, { all: true, verbatim: true }).catch(() => []),
1033
+ sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
1034
+ ]);
1035
+ return found ?? [];
1036
+ }
1037
+ async function answerOf(response) {
1038
+ const { status, ok, headers } = response;
1039
+ const reader = response.body?.getReader();
1040
+ if (!reader) {
1041
+ return { status, ok, body: "", truncated: false, headers };
1042
+ }
1043
+ const read = [];
1044
+ let bytes = 0;
1045
+ while (bytes <= BODY_LIMIT_BYTES) {
1046
+ const { done, value } = await reader.read();
1047
+ if (done) {
1048
+ return { status, ok, body: text(read), truncated: false, headers };
1049
+ }
1050
+ read.push(value);
1051
+ bytes += value.length;
1052
+ }
1053
+ await reader.cancel();
1054
+ return { status, ok, body: text(read), truncated: true, headers };
1055
+ }
1056
+ function withoutBody(sent) {
1057
+ const headers = { ...sent.headers };
1058
+ delete headers["content-type"];
1059
+ return { ...sent, method: "GET", headers, body: void 0 };
1060
+ }
1061
+ function withoutCredentials(sent) {
1062
+ const headers = { ...sent.headers };
1063
+ for (const named of CREDENTIALS) {
1064
+ delete headers[named];
1065
+ }
1066
+ return { ...sent, headers };
1067
+ }
1068
+ function text(chunks) {
1069
+ const decoder = new TextDecoder();
1070
+ let decoded2 = "";
1071
+ for (const chunk of chunks) {
1072
+ decoded2 += decoder.decode(chunk, { stream: true });
1073
+ }
1074
+ return decoded2 + decoder.decode();
1075
+ }
1076
+
1077
+ // ../core/refusal.ts
1078
+ var WalletRefused = class extends Error {
1079
+ reason;
1080
+ constructor(reason, message) {
1081
+ super(message);
1082
+ this.reason = reason;
1083
+ }
1084
+ };
1085
+ var NoWalletAvailable = class extends Error {
1086
+ wallets;
1087
+ constructor(wallets) {
1088
+ super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
1089
+ this.wallets = wallets;
1090
+ }
1091
+ };
1092
+
1093
+ // ../core/lnurl.ts
1094
+ var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1095
+ var RESOLVE_TIMEOUT_MS = 3e4;
1096
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
1097
+ var MIN_PACE_SECS = 1;
1098
+ var MAX_PACE_SECS = 3600;
1099
+ var MIN_PER_SECOND = 0.01;
1100
+ var MAX_PER_SECOND = 100;
1101
+ async function resolve(send, addresses, amountMsat) {
1102
+ const served = await firstThatServes(
1103
+ addresses,
1104
+ (address, deadline) => resolveAddress(send, address, amountMsat, deadline)
1105
+ );
1106
+ return served.won;
1107
+ }
1108
+ async function firstThatServes(addresses, attempt) {
1109
+ const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
1110
+ const refusals3 = [];
1111
+ for (const address of addresses) {
1112
+ if (deadline.aborted) {
1113
+ break;
1114
+ }
1115
+ try {
1116
+ return { won: await attempt(address, deadline), refusals: refusals3 };
1117
+ } catch (refusal) {
1118
+ if (!(refusal instanceof WalletRefused)) {
1119
+ throw refusal;
1120
+ }
1121
+ console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
1122
+ refusals3.push({ address, reason: refusal.reason });
1123
+ }
1124
+ }
1125
+ throw new NoWalletAvailable(refusals3);
677
1126
  }
678
- function minorScaleOf(currency) {
679
- return 10 ** minorUnitsOf(currency);
1127
+ async function probe(send, address, amountMsat, deadline) {
1128
+ const [user, domain] = splitAddress(address);
1129
+ if (cannotReleaseAPreimage(domain)) {
1130
+ throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
1131
+ }
1132
+ const pay = await answered(
1133
+ send,
1134
+ `https://${domain}/.well-known/lnurlp/${user}`,
1135
+ deadline
1136
+ );
1137
+ if (pay.tag !== "payRequest" || !pay.callback) {
1138
+ throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
1139
+ }
1140
+ if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
1141
+ throw new WalletRefused(
1142
+ "amount-not-accepted",
1143
+ `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
1144
+ );
1145
+ }
1146
+ return { address, pay };
680
1147
  }
681
-
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`);
1148
+ async function resolveAddress(send, address, amountMsat, deadline) {
1149
+ const { pay } = await probe(send, address, amountMsat, deadline);
1150
+ const invoice = await answered(
1151
+ send,
1152
+ withAmount(pay.callback, amountMsat),
1153
+ deadline
1154
+ );
1155
+ if (!invoice.pr) {
1156
+ throw new WalletRefused("unreachable", `${address} returned no invoice`);
1157
+ }
1158
+ if (!invoice.verify || !publicHttps(invoice.verify)) {
1159
+ throw new WalletRefused(
1160
+ "cannot-prove-delivery",
1161
+ `${address} publishes no usable LUD-21 verify URL`
1162
+ );
1163
+ }
1164
+ const issued = decodeIssued(address, invoice.pr);
1165
+ if (issued.amountMsat !== amountMsat) {
1166
+ throw new WalletRefused(
1167
+ "invoice-refused",
1168
+ `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
1169
+ );
1170
+ }
1171
+ if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
1172
+ throw new WalletRefused(
1173
+ "invoice-refused",
1174
+ `${address} does not bind its invoice to the metadata it serves`
1175
+ );
691
1176
  }
692
- const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
693
- wrapped.set(PKCS8_HEADER);
694
- wrapped.set(seed, PKCS8_HEADER.length);
695
- const key = await crypto.subtle.importKey("pkcs8", asBuffer2(wrapped), ALGORITHM, true, ["sign"]);
696
- const jwk = await crypto.subtle.exportKey("jwk", key);
697
1177
  return {
698
- publicKeyHex: bytesToHex(fromBase64Url2(jwk.x ?? "")),
699
- sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer2(payload))))
1178
+ address,
1179
+ bolt11: invoice.pr,
1180
+ verifyUrl: invoice.verify,
1181
+ paymentHash: issued.paymentHash,
1182
+ expiresAt: issued.expiresAt
700
1183
  };
701
1184
  }
702
- async function verifyHex(publicKeyHex, signatureHex, payload) {
703
- if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) {
704
- return false;
1185
+ async function answered(send, url, deadline) {
1186
+ try {
1187
+ return await fetchJson(send, url, deadline);
1188
+ } catch (cause) {
1189
+ throw new WalletRefused("unreachable", String(cause));
705
1190
  }
706
- if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) {
707
- return false;
1191
+ }
1192
+ function decodeIssued(address, bolt11) {
1193
+ const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
1194
+ if (paymentHash === null || expiresAt === null) {
1195
+ throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
1196
+ }
1197
+ return { paymentHash, descriptionHash, amountMsat, expiresAt };
1198
+ }
1199
+ async function checkSettled(send, verifyUrl, paymentHash) {
1200
+ const answer = await answeredJson(send, verifyUrl);
1201
+ const asked = {
1202
+ pace: paceAskedFor(answer.headers),
1203
+ ceiling: ceilingAskedFor(answer.headers)
1204
+ };
1205
+ if (!answer.said.settled || !answer.said.preimage) {
1206
+ return { preimage: null, ...asked };
1207
+ }
1208
+ if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
1209
+ throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
1210
+ }
1211
+ return { preimage: answer.said.preimage, ...asked };
1212
+ }
1213
+ function paceAskedFor(headers) {
1214
+ const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
1215
+ if (!asked) {
1216
+ return null;
1217
+ }
1218
+ return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
1219
+ }
1220
+ function ceilingAskedFor(headers) {
1221
+ const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
1222
+ if (!asked) {
1223
+ return null;
1224
+ }
1225
+ const perWindow = Number(asked[1]);
1226
+ const window = asked[2] === void 0 ? 1 : Number(asked[2]);
1227
+ if (perWindow === 0 || window === 0) {
1228
+ return null;
1229
+ }
1230
+ return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
1231
+ }
1232
+ function cannotReleaseAPreimage(host) {
1233
+ const lowered = host.toLowerCase();
1234
+ return VERIFY_WITHOUT_PREIMAGE.some(
1235
+ (known) => lowered === known || lowered.endsWith(`.${known}`)
1236
+ );
1237
+ }
1238
+ function withAmount(callback, amountMsat) {
1239
+ const separator = callback.includes("?") ? "&" : "?";
1240
+ return `${callback}${separator}amount=${amountMsat}`;
1241
+ }
1242
+ function splitAddress(address) {
1243
+ const [user, domain, ...rest] = address.split("@");
1244
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
1245
+ throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
708
1246
  }
1247
+ return [user, domain];
1248
+ }
1249
+ function isNothingButAHost(domain) {
709
1250
  try {
710
- const key = await crypto.subtle.importKey(
711
- "raw",
712
- asBuffer2(hexToBytes(publicKeyHex)),
713
- ALGORITHM,
714
- false,
715
- ["verify"]
716
- );
717
- return await crypto.subtle.verify(
718
- ALGORITHM,
719
- key,
720
- asBuffer2(hexToBytes(signatureHex)),
721
- asBuffer2(payload)
722
- );
1251
+ const parsed = new URL(`https://${domain}/`);
1252
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
723
1253
  } catch {
724
1254
  return false;
725
1255
  }
726
1256
  }
727
- function fromBase64Url2(text2) {
728
- const padded = text2.replace(/-/g, "+").replace(/_/g, "/");
729
- const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
730
- const bytes = new Uint8Array(raw.length);
731
- for (let at = 0; at < raw.length; at++) {
732
- bytes[at] = raw.charCodeAt(at);
1257
+ async function fetchJson(send, url, deadline) {
1258
+ return (await answeredJson(send, url, deadline)).said;
1259
+ }
1260
+ async function answeredJson(send, url, deadline) {
1261
+ const answer = await ask(send, url, { headers: { accept: "application/json" }, deadline });
1262
+ if (!answer.ok) {
1263
+ throw new Error(`${url} answered ${answer.status}`);
733
1264
  }
734
- return bytes;
1265
+ if (answer.truncated) {
1266
+ throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
1267
+ }
1268
+ return { said: JSON.parse(answer.body), headers: answer.headers };
735
1269
  }
736
- function asBuffer2(bytes) {
737
- return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1270
+
1271
+ // ../core/pinned.ts
1272
+ var HTTPS_PORT = 443;
1273
+ var CARRIES_NO_BODY = [204, 205, 304];
1274
+ var pinnedToTheAddressWeVerified = async (url, sent, signal, at) => {
1275
+ const asked = new URL(url);
1276
+ const first = at[0];
1277
+ if (first === void 0) {
1278
+ throw new Error(`${url} resolved to nothing worth connecting to`);
1279
+ }
1280
+ const { request } = await import("https");
1281
+ const { Readable } = await import("stream");
1282
+ return new Promise((settle, fail) => {
1283
+ if (signal.aborted) {
1284
+ fail(signal.reason);
1285
+ return;
1286
+ }
1287
+ const call = request(reaching(asked, sent, at, first), (answer) => {
1288
+ const status = answer.statusCode ?? 502;
1289
+ settle(
1290
+ new Response(
1291
+ CARRIES_NO_BODY.includes(status) ? null : Readable.toWeb(answer),
1292
+ { status, headers: headersOf(answer.headers) }
1293
+ )
1294
+ );
1295
+ });
1296
+ const give = () => call.destroy(new Error(`${url} took longer than allowed`));
1297
+ signal.addEventListener("abort", give, { once: true });
1298
+ call.on("close", () => signal.removeEventListener("abort", give));
1299
+ call.on("error", fail);
1300
+ if (sent.body !== void 0) {
1301
+ call.write(sent.body);
1302
+ }
1303
+ call.end();
1304
+ });
1305
+ };
1306
+ function reaching(asked, sent, at, first) {
1307
+ return {
1308
+ host: asked.hostname,
1309
+ servername: asked.hostname,
1310
+ port: asked.port === "" ? HTTPS_PORT : Number(asked.port),
1311
+ path: `${asked.pathname}${asked.search}`,
1312
+ method: sent.method ?? "GET",
1313
+ headers: { ...sent.headers, host: asked.host },
1314
+ agent: false,
1315
+ lookup: (_name, options, done) => {
1316
+ if (options.all === true) {
1317
+ done(
1318
+ null,
1319
+ at.map((one) => ({ ...one }))
1320
+ );
1321
+ } else {
1322
+ done(null, first.address, first.family);
1323
+ }
1324
+ }
1325
+ };
1326
+ }
1327
+ function headersOf(raw) {
1328
+ const headers = new Headers();
1329
+ for (const [name, value] of Object.entries(raw)) {
1330
+ if (Array.isArray(value)) {
1331
+ for (const one of value) {
1332
+ headers.append(name, one);
1333
+ }
1334
+ } else if (value !== void 0) {
1335
+ headers.set(name, value);
1336
+ }
1337
+ }
1338
+ return headers;
738
1339
  }
739
1340
 
740
1341
  // src/wire.ts
@@ -747,19 +1348,19 @@ var REASONS = /* @__PURE__ */ new Set([
747
1348
  "cannot-prove-delivery",
748
1349
  "invoice-refused"
749
1350
  ]);
750
- function createRequestBody(params, trigger, replay) {
1351
+ function createRequestBody(priced2, webhookUrl, trigger, replay) {
751
1352
  return JSON.stringify({
752
- ln_addresses: params.lnAddresses,
753
- incoming_amount: toAmount(params.amountMsat),
754
- webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0,
1353
+ ln_addresses: priced2.paidTo,
1354
+ incoming_amount: toAmount(priced2.amountMsat),
1355
+ webhook: webhookUrl ? { url: webhookUrl } : void 0,
755
1356
  trigger: trigger ?? void 0,
756
1357
  replay
757
1358
  });
758
1359
  }
759
- function quoteRequestBody(params) {
1360
+ function quoteRequestBody(priced2) {
760
1361
  return JSON.stringify({
761
- ln_addresses: params.lnAddresses,
762
- amount: toAmount(params.amountMsat)
1362
+ ln_addresses: priced2.paidTo,
1363
+ amount: toAmount(priced2.amountMsat)
763
1364
  });
764
1365
  }
765
1366
  function quoteFromWire(body) {
@@ -767,12 +1368,12 @@ function quoteFromWire(body) {
767
1368
  if (wire === null) {
768
1369
  return null;
769
1370
  }
770
- const lnAddress = text(wire["ln_address"]);
1371
+ const lnAddress = text2(wire["ln_address"]);
771
1372
  const amountMsat = msatFrom(wire["amount"], 1);
772
1373
  const feeMsat = msatFrom(wire["fee"], 0);
773
1374
  const minMsat = msatFrom(wire["min_amount"], 0);
774
1375
  const maxMsat = msatFrom(wire["max_amount"], 0);
775
- const metadata = text(wire["metadata"]);
1376
+ const metadata = text2(wire["metadata"]);
776
1377
  const refusals3 = refusalsFrom(wire["refusals"]);
777
1378
  if (lnAddress === null || amountMsat === null || feeMsat === null) {
778
1379
  return null;
@@ -787,57 +1388,66 @@ function paymentFromWire(body) {
787
1388
  if (wire === null) {
788
1389
  return null;
789
1390
  }
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);
1391
+ const id = text2(wire["id"]);
1392
+ const paymentHash = text2(wire["payment_hash"]);
1393
+ const verifyUrl = text2(wire["verify_url"]);
796
1394
  const expiresAt = secondsFrom(wire["expires_at"]);
797
1395
  const createdAt = secondsFrom(wire["created_at"]);
798
1396
  const status = wire["status"];
799
1397
  const preimage = wire["preimage"] ?? null;
800
- if (id === null || lnAddress === null || bolt11 === null) {
801
- return null;
802
- }
803
- if (paymentHash === null || verifyUrl === null) {
1398
+ const sealed = wire["sealed"] ?? null;
1399
+ if (id === null || paymentHash === null || verifyUrl === null) {
804
1400
  return null;
805
1401
  }
806
- if (amountMsat === null || expiresAt === null || createdAt === null) {
1402
+ if (expiresAt === null || createdAt === null || !isStatus(status)) {
807
1403
  return null;
808
1404
  }
809
- if (!isStatus(status)) {
1405
+ if (preimage !== null && typeof preimage !== "string") {
810
1406
  return null;
811
1407
  }
812
- if (preimage !== null && typeof preimage !== "string") {
1408
+ if (sealed !== null && typeof sealed !== "string") {
813
1409
  return null;
814
1410
  }
815
- return {
1411
+ const reported = {
816
1412
  id,
817
- lnAddress,
818
- amountMsat,
819
1413
  status,
820
1414
  paymentHash,
821
- bolt11,
1415
+ verifyUrl,
822
1416
  preimage,
823
1417
  expiresAt,
824
1418
  createdAt,
825
- verifyUrl
1419
+ sealed
826
1420
  };
1421
+ const lnAddress = text2(wire["ln_address"]);
1422
+ const amountMsat = wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1);
1423
+ const bolt11 = text2(wire["bolt11"]);
1424
+ if (lnAddress !== null && amountMsat !== null && bolt11 !== null) {
1425
+ return { ...reported, kind: "minted", lnAddress, amountMsat, bolt11 };
1426
+ }
1427
+ if (lnAddress === null && amountMsat === null && bolt11 === null) {
1428
+ return { ...reported, kind: "watched", lnAddress: null, amountMsat: null, bolt11: null };
1429
+ }
1430
+ return null;
1431
+ }
1432
+ function mintedFromWire(body) {
1433
+ const payment = paymentFromWire(body);
1434
+ return payment !== null && payment.kind === "minted" ? payment : null;
827
1435
  }
828
- function watchRequestBody(params, trigger) {
829
- const expiresAt = new Date(params.expiresAt * 1e3);
1436
+ function watchRequestBody(handover, trigger) {
1437
+ const expiresAt = new Date(handover.expiresAt * 1e3);
830
1438
  if (Number.isNaN(expiresAt.getTime())) {
831
- throw new TypeError(`expiresAt must be a usable unix time in seconds, got ${params.expiresAt}`);
1439
+ throw new TypeError(
1440
+ `expiresAt must be a usable unix time in seconds, got ${handover.expiresAt}`
1441
+ );
832
1442
  }
833
1443
  return JSON.stringify({
834
- payment_hash: params.paymentHash,
835
- verify_url: params.verifyUrl,
1444
+ payment_hash: handover.paymentHash,
1445
+ verify_url: handover.verifyUrl,
836
1446
  expires_at: expiresAt.toISOString(),
837
1447
  trigger: trigger ?? void 0,
838
- replay: params.replay,
839
- sealed: params.sealed,
840
- webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0
1448
+ replay: handover.replay,
1449
+ sealed: handover.sealed,
1450
+ webhook: handover.webhookUrl ? { url: handover.webhookUrl } : void 0
841
1451
  });
842
1452
  }
843
1453
  function settlementFromWire(body) {
@@ -845,8 +1455,8 @@ function settlementFromWire(body) {
845
1455
  if (wire === null) {
846
1456
  return null;
847
1457
  }
848
- const id = text(wire["id"]);
849
- const paymentHash = text(wire["payment_hash"]);
1458
+ const id = text2(wire["id"]);
1459
+ const paymentHash = text2(wire["payment_hash"]);
850
1460
  const settledAt = secondsFrom(wire["settled_at"]);
851
1461
  const status = wire["status"];
852
1462
  const preimage = wire["preimage"] ?? null;
@@ -858,66 +1468,20 @@ function settlementFromWire(body) {
858
1468
  }
859
1469
  return { id, status, paymentHash, preimage, settledAt };
860
1470
  }
861
- function triggerEventFromWire(body) {
862
- const wire = asObject(body);
863
- if (wire === null) {
864
- return null;
865
- }
866
- const id = text(wire["id"]);
867
- const paymentHash = text(wire["payment_hash"]);
868
- const verifyUrl = text(wire["verify_url"]);
869
- const expiresAt = secondsFrom(wire["expires_at"]);
870
- const createdAt = secondsFrom(wire["created_at"]);
871
- const status = wire["status"];
872
- const preimage = wire["preimage"] ?? null;
873
- const sealed = wire["sealed"] ?? null;
874
- if (id === null || paymentHash === null || verifyUrl === null) {
875
- return null;
876
- }
877
- if (expiresAt === null || createdAt === null || !isStatus(status)) {
878
- return null;
879
- }
880
- if (preimage !== null && typeof preimage !== "string") {
881
- return null;
882
- }
883
- if (sealed !== null && typeof sealed !== "string") {
884
- return null;
885
- }
886
- return {
887
- id,
888
- kind: kindOf(wire),
889
- paymentHash,
890
- verifyUrl,
891
- status,
892
- preimage,
893
- expiresAt,
894
- createdAt,
895
- sealed,
896
- lnAddress: text(wire["ln_address"]),
897
- amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
898
- };
899
- }
900
1471
  function socketTicketFromWire(body) {
901
1472
  const wire = asObject(body);
902
1473
  if (wire === null) {
903
1474
  return null;
904
1475
  }
905
- const ticket = text(wire["ticket"]);
1476
+ const ticket = text2(wire["ticket"]);
906
1477
  const expiresAt = secondsFrom(wire["expires_at"]);
907
1478
  if (ticket === null || expiresAt === null) {
908
1479
  return null;
909
1480
  }
910
1481
  return { ticket, expiresAt };
911
1482
  }
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 };
1483
+ function toAmount(msat2) {
1484
+ return { value: String(msat2), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
921
1485
  }
922
1486
  function asObject(value) {
923
1487
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -925,7 +1489,7 @@ function asObject(value) {
925
1489
  }
926
1490
  return value;
927
1491
  }
928
- function text(value) {
1492
+ function text2(value) {
929
1493
  return typeof value === "string" ? value : null;
930
1494
  }
931
1495
  function msatFrom(value, least) {
@@ -940,8 +1504,8 @@ function msatFrom(value, least) {
940
1504
  if (typeof digits !== "string" || !/^[0-9]+$/.test(digits)) {
941
1505
  return null;
942
1506
  }
943
- const msat = Number(digits);
944
- return Number.isSafeInteger(msat) && msat >= least ? msat : null;
1507
+ const msat2 = Number(digits);
1508
+ return Number.isSafeInteger(msat2) && msat2 >= least ? msat2 : null;
945
1509
  }
946
1510
  function refusalsFrom(value) {
947
1511
  if (!Array.isArray(value)) {
@@ -950,7 +1514,7 @@ function refusalsFrom(value) {
950
1514
  const refusals3 = [];
951
1515
  for (const entry of value) {
952
1516
  const failure = asObject(entry);
953
- const address = failure === null ? null : text(failure["address"]);
1517
+ const address = failure === null ? null : text2(failure["address"]);
954
1518
  const reason = failure?.["reason"];
955
1519
  if (address === null || typeof reason !== "string" || !REASONS.has(reason)) {
956
1520
  return null;
@@ -971,118 +1535,67 @@ function isStatus(value) {
971
1535
  }
972
1536
 
973
1537
  // src/webhook.ts
974
- var SIGNATURE_HEADER = "x-signature";
975
- var TIMESTAMP_HEADER = "x-timestamp";
1538
+ var SIGNATURE_HEADER2 = "x-signature";
1539
+ var TIMESTAMP_HEADER2 = "x-timestamp";
976
1540
  var GATEWAY_KEY_PREFIX = "ed25519=";
977
1541
  var DEFAULT_TOLERANCE_SECS = 300;
978
1542
  var CHALLENGE = "webhook-challenge";
979
1543
  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
- );
1544
+ async function readPayment(request, credential, options = {}) {
1545
+ const body = await believable(request, credential, options);
1546
+ return body === null ? null : decoded(body, paymentFromWire);
992
1547
  }
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
- }
1548
+ async function readSettlement(request, credential, options = {}) {
1549
+ const body = await believable(request, credential, options);
1550
+ return body === null ? null : decoded(body, settlementFromWire);
1003
1551
  }
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);
1552
+ async function answerWebhookChallenge(request, credential, options = {}) {
1553
+ const body = await believable(request, credential, options);
1554
+ const nonce = body === null ? null : nonceOf(body, CHALLENGE);
1555
+ return nonce === null ? null : answered2(nonce);
1011
1556
  }
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 {
1557
+ async function answerVerifyChallenge(request) {
1558
+ if (request.method !== "POST") {
1020
1559
  return null;
1021
1560
  }
1561
+ const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
1562
+ return nonce === null ? null : answered2(nonce);
1022
1563
  }
1023
- async function parseWatchedWebhookRequest(request, credential, options = {}) {
1024
- const signature = request.headers.get(SIGNATURE_HEADER);
1025
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1564
+ async function believable(request, credential, options) {
1565
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1566
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1026
1567
  if (signature === null || timestamp === null) {
1027
1568
  return null;
1028
1569
  }
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 {
1038
- return null;
1039
- }
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) {
1570
+ if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) {
1045
1571
  return null;
1046
1572
  }
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);
1054
- }
1055
- async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1056
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
1573
+ if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
1057
1574
  return null;
1058
1575
  }
1059
- const nonce = challenged(typeof body === "string" ? body : new TextDecoder().decode(body));
1060
- return nonce === null ? null : JSON.stringify({ nonce });
1576
+ const body = await request.clone().text();
1577
+ const signed = await verifyHex(
1578
+ credential.publicKey.toLowerCase(),
1579
+ signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1580
+ stamped(timestamp, body)
1581
+ );
1582
+ return signed ? body : null;
1061
1583
  }
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) {
1584
+ function decoded(body, from) {
1585
+ try {
1586
+ return from(JSON.parse(body));
1587
+ } catch {
1066
1588
  return null;
1067
1589
  }
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
- }
1077
- function textOf(body) {
1078
- return typeof body === "string" ? body : new TextDecoder().decode(body);
1079
1590
  }
1080
- function challenged(text2) {
1081
- return nonceOf(text2, CHALLENGE);
1591
+ function answered2(nonce) {
1592
+ return new Response(JSON.stringify({ nonce }), {
1593
+ headers: { "content-type": "application/json" }
1594
+ });
1082
1595
  }
1083
- function nonceOf(text2, type) {
1596
+ function nonceOf(text3, type) {
1084
1597
  try {
1085
- const said = JSON.parse(text2);
1598
+ const said = JSON.parse(text3);
1086
1599
  if (said["type"] !== type || typeof said["nonce"] !== "string") {
1087
1600
  return null;
1088
1601
  }
@@ -1091,17 +1604,6 @@ function nonceOf(text2, type) {
1091
1604
  return null;
1092
1605
  }
1093
1606
  }
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
1607
  function recent(timestamp, toleranceSecs) {
1106
1608
  const sent = Number(timestamp);
1107
1609
  if (!Number.isFinite(sent)) {
@@ -1109,14 +1611,8 @@ function recent(timestamp, toleranceSecs) {
1109
1611
  }
1110
1612
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1111
1613
  }
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;
1614
+ function stamped(timestamp, body) {
1615
+ return new TextEncoder().encode(`${timestamp}.${body}`);
1120
1616
  }
1121
1617
 
1122
1618
  // src/bank.ts
@@ -1125,10 +1621,10 @@ var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
1125
1621
  var DEFAULT_POLL_EVERY_SECS = 30;
1126
1622
  var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
1127
1623
  var FORBIDDEN_IN_SPD = "*";
1128
- async function bankTransfer(params) {
1624
+ async function bankTransfer(gateway, params) {
1129
1625
  const currency = params.currency ?? DEFAULT_CURRENCY;
1130
1626
  refuseUnusable(params, currency);
1131
- await refuseAnOpenGateway(params);
1627
+ await refuseAnOpenGateway(gateway, params);
1132
1628
  const spd = shortPaymentDescriptor(params, currency);
1133
1629
  const subject = subjectOf(params.reference, params.amountMinor, currency);
1134
1630
  const polling = new URL(params.verifyUrl);
@@ -1137,7 +1633,7 @@ async function bankTransfer(params) {
1137
1633
  polling.searchParams.set("cc", currency);
1138
1634
  polling.searchParams.set("sig", await hmacHex(params.secret, `verify|${subject}`));
1139
1635
  const verifyUrl = polling.toString();
1140
- const watched = await params.gateway.watchPayment({
1636
+ const watched = await gateway.watch({
1141
1637
  paymentHash: hashOf(await hmacHex(params.secret, `preimage|${subject}`)),
1142
1638
  verifyUrl,
1143
1639
  expiresAt: params.expiresAt,
@@ -1153,7 +1649,7 @@ function bankVerifyEndpoint(config) {
1153
1649
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1154
1650
  };
1155
1651
  return async (request) => {
1156
- const consented = await answerVerifyChallengeRequest(request);
1652
+ const consented = await answerVerifyChallenge(request);
1157
1653
  if (consented !== null) {
1158
1654
  return consented;
1159
1655
  }
@@ -1214,11 +1710,11 @@ function shortPaymentDescriptor(params, currency) {
1214
1710
  function major(amountMinor, currency) {
1215
1711
  return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
1216
1712
  }
1217
- async function refuseAnOpenGateway(params) {
1713
+ async function refuseAnOpenGateway(gateway, params) {
1218
1714
  if (params.allowPublicGateway === true) {
1219
1715
  return;
1220
1716
  }
1221
- if (await params.gateway.refusesStrangers()) {
1717
+ if (await gateway.refusesStrangers()) {
1222
1718
  return;
1223
1719
  }
1224
1720
  throw new Error(
@@ -1242,124 +1738,240 @@ function refuseUnusable(params, currency) {
1242
1738
  throw new Error("a variable symbol is up to ten digits");
1243
1739
  }
1244
1740
  }
1245
- function unixNow() {
1246
- return Math.floor(Date.now() / 1e3);
1741
+ function unixNow() {
1742
+ return Math.floor(Date.now() / 1e3);
1743
+ }
1744
+
1745
+ // src/qr.ts
1746
+ var import_uqr = require("uqr");
1747
+ var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1748
+ function toLightningUri(destination) {
1749
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1750
+ return `LIGHTNING:${scanned}`;
1751
+ }
1752
+ function invoiceToSvg(destination, options) {
1753
+ return svgOf(toLightningUri(destination), options);
1754
+ }
1755
+ function svgOf(scanned, options) {
1756
+ const size = options?.size ?? 256;
1757
+ const color = options?.color ?? "#000";
1758
+ if (!SAFE_COLOR.test(color)) {
1759
+ throw new Error(`Invalid color: ${color}`);
1760
+ }
1761
+ return renderSvg(scanned, size, color);
1762
+ }
1763
+ function isLnAddress(destination) {
1764
+ const at = destination.indexOf("@");
1765
+ return at > 0 && destination.slice(at + 1).includes(".");
1766
+ }
1767
+ function renderSvg(content, size, color) {
1768
+ const { data } = (0, import_uqr.encode)(content);
1769
+ const modules = data.length;
1770
+ const cellSize = size / modules;
1771
+ let path = "";
1772
+ for (let y = 0; y < modules; y++) {
1773
+ for (let x = 0; x < modules; x++) {
1774
+ if (data[y][x]) {
1775
+ path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1776
+ }
1777
+ }
1778
+ }
1779
+ return [
1780
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1781
+ `<rect width="100%" height="100%" fill="white"/>`,
1782
+ `<path d="${path}" fill="${color}"/>`,
1783
+ `</svg>`
1784
+ ].join("");
1785
+ }
1786
+
1787
+ // src/relay.ts
1788
+ var DEFAULT_POLL_EVERY_SECS2 = 5;
1789
+ var WALLET = "w";
1790
+ function lightningVerifyEndpoint(config) {
1791
+ const paced = {
1792
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
1793
+ };
1794
+ return async (request) => {
1795
+ const consented = await answerVerifyChallenge(request);
1796
+ if (consented !== null) {
1797
+ return consented;
1798
+ }
1799
+ const sealed = new URL(request.url).searchParams.get(WALLET);
1800
+ if (sealed === null) {
1801
+ return Response.json({ settled: false }, { status: 400 });
1802
+ }
1803
+ const opened = await unseal(config.secret, sealed);
1804
+ if (opened === null) {
1805
+ return Response.json({ settled: false }, { status: 403 });
1806
+ }
1807
+ const wallet = JSON.parse(opened);
1808
+ const asked = await checkSettled(
1809
+ config.send ?? pinnedToTheAddressWeVerified,
1810
+ wallet.url,
1811
+ wallet.hash
1812
+ ).catch(() => null);
1813
+ if (asked === null) {
1814
+ return Response.json({ settled: false }, { status: 502 });
1815
+ }
1816
+ return Response.json(
1817
+ { settled: asked.preimage !== null, preimage: asked.preimage },
1818
+ { headers: paced }
1819
+ );
1820
+ };
1821
+ }
1822
+ async function relayedVerifyUrl(endpoint, wallet, secret) {
1823
+ const relayed = new URL(endpoint);
1824
+ relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
1825
+ return relayed.toString();
1247
1826
  }
1248
1827
 
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)));
1828
+ // src/rail.ts
1829
+ var BANK = "bank";
1830
+ var LIGHTNING = "lightning";
1831
+ function bankRail(gateway, config) {
1832
+ return async (order) => {
1833
+ const expiresAt = config.expiresAt(order);
1834
+ const transfer = await bankTransfer(gateway, {
1835
+ secret: config.secret,
1836
+ iban: config.iban,
1837
+ verifyUrl: config.verifyUrl,
1838
+ reference: order.reference,
1839
+ amountMinor: order.amountMinor,
1840
+ currency: order.currency,
1841
+ expiresAt,
1842
+ trigger: config.trigger,
1843
+ replay: config.replay,
1844
+ sealed: await config.sealed?.(order),
1845
+ variableSymbol: config.variableSymbol?.(order),
1846
+ webhookUrl: config.webhookUrl,
1847
+ allowPublicGateway: config.allowPublicGateway
1848
+ });
1849
+ return {
1850
+ id: transfer.id,
1851
+ rail: config.name ?? BANK,
1852
+ scan: transfer.spd,
1853
+ qr: transfer.spd,
1854
+ expiresAt
1855
+ };
1856
+ };
1257
1857
  }
1258
- function paymentNamedBy(caller, paymentHash) {
1259
- return sha256Hex(`${caller}.payment-id.${paymentHash}`);
1858
+ function lightningRail(gateway, config) {
1859
+ return async (order) => {
1860
+ const payment = await gateway.mint(
1861
+ {
1862
+ paidTo: config.paidTo,
1863
+ amount: await msatForOrder(order, config.amount, config.rate),
1864
+ webhookUrl: config.webhookUrl
1865
+ },
1866
+ {
1867
+ idempotencyKey: config.idempotencyKey?.(order),
1868
+ trigger: config.trigger,
1869
+ replay: config.replay
1870
+ }
1871
+ );
1872
+ return {
1873
+ id: payment.id,
1874
+ rail: config.name ?? LIGHTNING,
1875
+ scan: payment.bolt11,
1876
+ qr: toLightningUri(payment.bolt11),
1877
+ expiresAt: payment.expiresAt
1878
+ };
1879
+ };
1260
1880
  }
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))}`
1881
+ function blindLightningRail(gateway, config) {
1882
+ return async (order) => {
1883
+ const resolved2 = await invoiceFrom(
1884
+ config.paidTo,
1885
+ await msatForOrder(order, config.amount, config.rate),
1886
+ config.send
1887
+ );
1888
+ const relay = config.relayThrough;
1889
+ const watched = await gateway.watch({
1890
+ paymentHash: resolved2.paymentHash,
1891
+ verifyUrl: relay ? await relayedVerifyUrl(
1892
+ relay.endpoint,
1893
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
1894
+ relay.secret
1895
+ ) : resolved2.verifyUrl,
1896
+ expiresAt: resolved2.expiresAt,
1897
+ trigger: config.trigger,
1898
+ replay: config.replay,
1899
+ sealed: await config.sealed?.(order),
1900
+ webhookUrl: config.webhookUrl
1901
+ });
1902
+ return {
1903
+ id: watched.id,
1904
+ rail: config.name ?? LIGHTNING,
1905
+ scan: resolved2.bolt11,
1906
+ qr: toLightningUri(resolved2.bolt11),
1907
+ expiresAt: resolved2.expiresAt
1908
+ };
1267
1909
  };
1268
1910
  }
1269
- function spoken(timestamp, method, path, body) {
1270
- return new TextEncoder().encode(
1271
- `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
1272
- );
1911
+ async function invoiceFrom(paidTo, amount, send = pinnedToTheAddressWeVerified) {
1912
+ const addresses = typeof paidTo === "string" ? [paidTo] : paidTo;
1913
+ try {
1914
+ return await resolve(send, addresses, await amountNow(amount));
1915
+ } catch (refused) {
1916
+ if (refused instanceof NoWalletAvailable) {
1917
+ throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
1918
+ }
1919
+ throw refused;
1920
+ }
1273
1921
  }
1274
-
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;
1922
+ async function msatForOrder(order, amount, rate) {
1923
+ if (amount !== void 0) {
1924
+ return await amountNow(amount(order));
1284
1925
  }
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;
1926
+ return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
1927
+ }
1928
+ var Rails = class {
1929
+ constructor(gateway) {
1930
+ this.gateway = gateway;
1292
1931
  }
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;
1932
+ gateway;
1933
+ /** Lightning, with the gateway minting against a priority list of addresses */
1934
+ lightning(config) {
1935
+ return lightningRail(this.gateway, config);
1302
1936
  }
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;
1937
+ /**
1938
+ * Lightning, with the invoice resolved here so the gateway is told neither the
1939
+ * address nor the amount
1940
+ */
1941
+ blindLightning(config) {
1942
+ return blindLightningRail(this.gateway, config);
1317
1943
  }
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;
1944
+ /** A bank transfer, proved the way a Lightning payment is */
1945
+ bank(config) {
1946
+ return bankRail(this.gateway, config);
1332
1947
  }
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;
1948
+ /**
1949
+ * One bank transfer without building a rail first, for a shop that asks for
1950
+ * them one at a time rather than beside another payment method
1951
+ */
1952
+ transfer(params) {
1953
+ return bankTransfer(this.gateway, params);
1340
1954
  }
1341
1955
  };
1342
1956
 
1343
1957
  // src/verify.ts
1344
- var HTTP_TIMEOUT_MS = 15e3;
1958
+ var HTTP_TIMEOUT_MS2 = 15e3;
1345
1959
  var DEFAULT_WRAP_PROPORTION = 0.01;
1346
1960
  var DEFAULT_WRAP_BASE_MSAT = 1e3;
1347
- async function proveOrigin(payment, request) {
1961
+ async function proveOrigin(payment, asked) {
1348
1962
  const cheat = (code) => new GatewayCheatError(code, payment.id);
1349
- const listed = request.lnAddresses.find(
1350
- (address) => equalIgnoringCase(address, payment.lnAddress)
1351
- );
1963
+ const listed = asked.paidTo.find((address) => equalIgnoringCase(address, payment.lnAddress));
1352
1964
  if (listed === void 0) {
1353
1965
  throw cheat("address_not_requested");
1354
1966
  }
1355
- if (payment.amountMsat !== request.amountMsat) {
1967
+ if (payment.amountMsat !== asked.amountMsat) {
1356
1968
  throw cheat("amount_mismatch");
1357
1969
  }
1358
1970
  const invoice = decodeInvoice(payment.bolt11);
1359
1971
  if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
1360
1972
  throw cheat("hash_mismatch");
1361
1973
  }
1362
- if (invoice.amountMsat !== request.amountMsat) {
1974
+ if (invoice.amountMsat !== asked.amountMsat) {
1363
1975
  throw cheat("amount_mismatch");
1364
1976
  }
1365
1977
  const payRequest = await payRequestFor(listed, payment);
@@ -1377,8 +1989,8 @@ async function proveOrigin(payment, request) {
1377
1989
  throw cheat("invoice_not_issued");
1378
1990
  }
1379
1991
  }
1380
- async function proveSettlement(payment, request) {
1381
- await proveOrigin(payment, request);
1992
+ async function proveSettlement(payment, asked) {
1993
+ await proveOrigin(payment, asked);
1382
1994
  const verified = await reachable(payment.verifyUrl, payment);
1383
1995
  if (verified.settled !== true || typeof verified.preimage !== "string") {
1384
1996
  return null;
@@ -1388,46 +2000,48 @@ async function proveSettlement(payment, request) {
1388
2000
  }
1389
2001
  return verified.preimage;
1390
2002
  }
1391
- function isProvablyPaid(payment) {
1392
- if (payment.status !== "paid" || payment.preimage === null) {
2003
+ function carriesProof(report) {
2004
+ if (report.status !== "paid" || report.preimage === null) {
1393
2005
  return false;
1394
2006
  }
1395
- const invoiceHash = decodeInvoice(payment.bolt11).paymentHash;
1396
- if (invoiceHash === null || !equalIgnoringCase(invoiceHash, payment.paymentHash)) {
1397
- return false;
2007
+ if (report.bolt11 !== void 0 && report.bolt11 !== null) {
2008
+ const invoiceHash = decodeInvoice(report.bolt11).paymentHash;
2009
+ if (invoiceHash === null || !equalIgnoringCase(invoiceHash, report.paymentHash)) {
2010
+ return false;
2011
+ }
1398
2012
  }
1399
- return preimageMatchesHash(payment.preimage, invoiceHash);
2013
+ return preimageMatchesHash(report.preimage, report.paymentHash);
1400
2014
  }
1401
2015
  function wrapFeeCeiling(amountMsat, allowance) {
1402
2016
  const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
1403
- return Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional));
2017
+ return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
1404
2018
  }
1405
2019
  function proveWrapped(wrapped, recipient, allowance) {
1406
2020
  const offered = decodeInvoice(wrapped);
1407
2021
  const owed = decodeInvoice(recipient);
1408
- const refuse = (code, detail) => {
2022
+ const refuse2 = (code, detail) => {
1409
2023
  throw new WrapRefusedError(code, detail);
1410
2024
  };
1411
2025
  if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
1412
- refuse("undecodable", "the wrapped invoice carries no hash, amount or expiry");
2026
+ refuse2("undecodable", "the wrapped invoice carries no hash, amount or expiry");
1413
2027
  }
1414
2028
  if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
1415
- refuse("undecodable", "the recipient's invoice carries no hash, amount or expiry");
2029
+ refuse2("undecodable", "the recipient's invoice carries no hash, amount or expiry");
1416
2030
  }
1417
2031
  if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
1418
- refuse("hash_mismatch", "settling the wrap would not settle the recipient");
2032
+ refuse2("hash_mismatch", "settling the wrap would not settle the recipient");
1419
2033
  }
1420
2034
  const asked = offered.amountMsat ?? 0;
1421
2035
  const due = owed.amountMsat ?? 0;
1422
2036
  if (asked < due) {
1423
- refuse("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
2037
+ refuse2("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
1424
2038
  }
1425
2039
  const ceiling = wrapFeeCeiling(due, allowance);
1426
2040
  if (asked - due > ceiling) {
1427
- refuse("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
2041
+ refuse2("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
1428
2042
  }
1429
2043
  if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
1430
- refuse("recipient_expires_first", "the operator could be paid and find it cannot deliver");
2044
+ refuse2("recipient_expires_first", "the operator could be paid and find it cannot deliver");
1431
2045
  }
1432
2046
  }
1433
2047
  async function payRequestFor(listed, payment) {
@@ -1445,7 +2059,7 @@ async function reachable(url, payment) {
1445
2059
  throw new Error(`${url} is not a public https URL`);
1446
2060
  }
1447
2061
  const response = await fetch(url, {
1448
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
2062
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2),
1449
2063
  headers: { accept: "application/json" }
1450
2064
  });
1451
2065
  if (!response.ok) {
@@ -1464,6 +2078,280 @@ function equalIgnoringCase(one, other) {
1464
2078
  return one.toLowerCase() === other.toLowerCase();
1465
2079
  }
1466
2080
 
2081
+ // src/request.ts
2082
+ function paymentRequestOf(gateway, payment, options) {
2083
+ return {
2084
+ id: payment.id,
2085
+ bolt11: payment.bolt11,
2086
+ paymentHash: payment.paymentHash,
2087
+ lnAddress: payment.lnAddress,
2088
+ amountMsat: payment.amountMsat,
2089
+ expiresAt: payment.expiresAt,
2090
+ qr: invoiceToSvg(payment.bolt11, options?.qr),
2091
+ payment,
2092
+ paid: async (waiting) => paidOnly(await gateway.settled(payment.id, { ...waitingOf(options), ...waiting })),
2093
+ prove: () => proveSettlement(payment, { paidTo: [payment.lnAddress], amountMsat: payment.amountMsat }),
2094
+ onPaid: (arrived, failed) => {
2095
+ const stop = new AbortController();
2096
+ const waiting = waitingOf(options);
2097
+ const signal = waiting.signal ? AbortSignal.any([stop.signal, waiting.signal]) : stop.signal;
2098
+ gateway.settled(payment.id, { ...waiting, signal }).then((ended) => arrived(paidOnly(ended))).catch((reason) => {
2099
+ if (!stop.signal.aborted) {
2100
+ failed?.(reason);
2101
+ }
2102
+ });
2103
+ return () => stop.abort();
2104
+ }
2105
+ };
2106
+ }
2107
+ function paidOnly(ended) {
2108
+ if (ended.status !== "paid") {
2109
+ throw new Error(`payment ${ended.id} ended ${ended.status} rather than paid`);
2110
+ }
2111
+ if (ended.kind !== "minted") {
2112
+ throw new Error(`payment ${ended.id} came back without the invoice it was minted with`);
2113
+ }
2114
+ return ended;
2115
+ }
2116
+ function waitingOf(options) {
2117
+ return { signal: options?.signal, tickets: options?.tickets };
2118
+ }
2119
+
2120
+ // src/trigger.ts
2121
+ var NONCE_BYTES = 16;
2122
+ function lnurlPayEndpoint(gateway, config) {
2123
+ return async (request) => {
2124
+ const url = new URL(request.url);
2125
+ const asked = url.searchParams.get("to");
2126
+ try {
2127
+ return asked === null ? await offer(gateway, config, url) : await mint(gateway, config, url, asked);
2128
+ } catch (failure) {
2129
+ return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
2130
+ }
2131
+ };
2132
+ }
2133
+ function watchTicketEndpoint(gateway, config) {
2134
+ return async (request) => {
2135
+ const offered = await offeredSecret(request);
2136
+ if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
2137
+ return Response.json({ reason: "not the watch secret" }, { status: 403 });
2138
+ }
2139
+ return await issue(gateway, config);
2140
+ };
2141
+ }
2142
+ function publicWatchTicketEndpoint(gateway, config) {
2143
+ return () => issue(gateway, config);
2144
+ }
2145
+ async function offer(gateway, config, url) {
2146
+ const { least, most } = await spread(config.amount);
2147
+ const quote = await gateway.quote({ paidTo: config.paidTo, amount: msat(least) });
2148
+ const nonce = randomNonce();
2149
+ const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
2150
+ callback.searchParams.set("to", quote.lnAddress);
2151
+ callback.searchParams.set("least", String(least));
2152
+ callback.searchParams.set("most", String(most));
2153
+ callback.searchParams.set("n", nonce);
2154
+ callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, least, most, nonce));
2155
+ return Response.json({
2156
+ tag: "payRequest",
2157
+ callback: callback.toString(),
2158
+ metadata: quote.metadata,
2159
+ minSendable: least,
2160
+ maxSendable: most
2161
+ });
2162
+ }
2163
+ async function spread(amount) {
2164
+ if (typeof amount === "number" || typeof amount === "function") {
2165
+ const pinned = await amountNow(amount);
2166
+ return { least: pinned, most: pinned };
2167
+ }
2168
+ const least = await amountNow(amount.least);
2169
+ const most = await amountNow(amount.most);
2170
+ if (most < least) {
2171
+ throw new Error(`a range cannot end at ${most} msat when it starts at ${least}`);
2172
+ }
2173
+ return { least, most };
2174
+ }
2175
+ async function mint(gateway, config, url, address) {
2176
+ const least = Number(url.searchParams.get("least"));
2177
+ const most = Number(url.searchParams.get("most"));
2178
+ const nonce = url.searchParams.get("n") ?? "";
2179
+ const signature = url.searchParams.get("sig") ?? "";
2180
+ const expected = await sign(config.secret, address, least, most, nonce);
2181
+ if (!equalInConstantTime(signature, expected)) {
2182
+ return refuse("this callback was not signed here");
2183
+ }
2184
+ const asked = url.searchParams.get("amount");
2185
+ const amountMsat = asked === null ? least : Number(asked);
2186
+ if (!Number.isSafeInteger(amountMsat) || amountMsat < least || amountMsat > most) {
2187
+ return refuse(
2188
+ least === most ? `this trigger costs exactly ${least} msat` : `this trigger takes between ${least} and ${most} msat`
2189
+ );
2190
+ }
2191
+ const minted = config.blind ? await mintBlind(gateway, config, address, amountMsat) : await mintThroughGateway(gateway, config, address, amountMsat, nonce);
2192
+ return Response.json({
2193
+ status: "OK",
2194
+ pr: minted.bolt11,
2195
+ routes: [],
2196
+ verify: minted.verifyUrl
2197
+ });
2198
+ }
2199
+ async function mintThroughGateway(gateway, config, address, amountMsat, nonce) {
2200
+ const payment = await gateway.mint(
2201
+ { paidTo: address, amount: msat(amountMsat) },
2202
+ { idempotencyKey: nonce, trigger: config.watchSecret, replay: config.replay }
2203
+ );
2204
+ return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
2205
+ }
2206
+ async function mintBlind(gateway, config, address, amountMsat) {
2207
+ const resolved2 = await resolve(
2208
+ config.send ?? pinnedToTheAddressWeVerified,
2209
+ [address],
2210
+ amountMsat
2211
+ );
2212
+ const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
2213
+ const locked = config.sealed;
2214
+ const relay = config.relayThrough;
2215
+ try {
2216
+ await gateway.watch({
2217
+ paymentHash: resolved2.paymentHash,
2218
+ verifyUrl: relay ? await relayedVerifyUrl(
2219
+ relay.endpoint,
2220
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
2221
+ relay.secret
2222
+ ) : resolved2.verifyUrl,
2223
+ expiresAt: resolved2.expiresAt,
2224
+ trigger: config.watchSecret,
2225
+ replay: config.replay,
2226
+ sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
2227
+ });
2228
+ } catch (refused) {
2229
+ if (!alreadyWatched(refused)) {
2230
+ throw refused;
2231
+ }
2232
+ }
2233
+ return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
2234
+ }
2235
+ function alreadyWatched(refused) {
2236
+ return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
2237
+ }
2238
+ function sign(secret, address, least, most, nonce) {
2239
+ return hmacHex(secret, `${address}|${least}|${most}|${nonce}`);
2240
+ }
2241
+ function randomNonce() {
2242
+ const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
2243
+ let hex = "";
2244
+ for (const byte of bytes) {
2245
+ hex += byte.toString(16).padStart(2, "0");
2246
+ }
2247
+ return hex;
2248
+ }
2249
+ function refuse(reason) {
2250
+ return Response.json({ status: "ERROR", reason });
2251
+ }
2252
+ async function issue(gateway, config) {
2253
+ const issued = await gateway.ticket(config.watchSecret, { replay: config.replay });
2254
+ return Response.json({
2255
+ ticket: issued.ticket,
2256
+ expires_at: new Date(issued.expiresAt * 1e3).toISOString()
2257
+ });
2258
+ }
2259
+ async function offeredSecret(request) {
2260
+ const body = await request.json().catch(() => null);
2261
+ return typeof body?.secret === "string" ? body.secret : "";
2262
+ }
2263
+
2264
+ // src/serving.ts
2265
+ var Serve = class {
2266
+ constructor(gateway) {
2267
+ this.gateway = gateway;
2268
+ }
2269
+ gateway;
2270
+ /**
2271
+ * An LNURL-pay endpoint of your own, standing in front of a priority list of
2272
+ * addresses, so a printed QR points at your domain and never expires
2273
+ */
2274
+ lnurlPay(config) {
2275
+ return lnurlPayEndpoint(this.gateway, config);
2276
+ }
2277
+ /** Trades the watch secret for a one minute socket ticket, refusing anyone without it */
2278
+ watchTicket(config) {
2279
+ return watchTicketEndpoint(this.gateway, config);
2280
+ }
2281
+ /**
2282
+ * Mints a socket ticket for anybody who asks, which makes the trigger's whole
2283
+ * stream public, preimages included. Only for a board where that is the point
2284
+ */
2285
+ publicWatchTicket(config) {
2286
+ return publicWatchTicketEndpoint(this.gateway, config);
2287
+ }
2288
+ /**
2289
+ * A verify endpoint of your own that asks the recipient's wallet for you, so
2290
+ * the gateway polls you and never the wallet
2291
+ */
2292
+ verify(config) {
2293
+ return lightningVerifyEndpoint(config);
2294
+ }
2295
+ /** The verify endpoint a bank rail is polled at, answering off your own statement */
2296
+ bankVerify(config) {
2297
+ return bankVerifyEndpoint(config);
2298
+ }
2299
+ /**
2300
+ * The whole webhook route: it answers the gateway's challenge, checks the
2301
+ * signature against the key the gateway publishes, refuses a settlement that
2302
+ * proves nothing, and calls you for the one that does.
2303
+ *
2304
+ * `export const POST = gateway.serve.webhook({ onSettled: fulfil })` is the
2305
+ * entire integration
2306
+ */
2307
+ webhook(handlers) {
2308
+ const options = { toleranceSecs: handlers.toleranceSecs };
2309
+ return async (request) => {
2310
+ const credential = await this.credential(handlers);
2311
+ const challenge = await answerWebhookChallenge(request, credential, options);
2312
+ if (challenge !== null) {
2313
+ return challenge;
2314
+ }
2315
+ const settlement = await readSettlement(request, credential, options);
2316
+ if (settlement !== null) {
2317
+ return await acted(settlement, handlers);
2318
+ }
2319
+ const payment = await readPayment(request, credential, options);
2320
+ if (payment !== null) {
2321
+ await handlers.onPayment?.(payment);
2322
+ return new Response("ok");
2323
+ }
2324
+ return new Response("bad signature", { status: 401 });
2325
+ };
2326
+ }
2327
+ /** Verify a delivery and read the settlement out of it, null when it is not believable */
2328
+ async readSettlement(request, options) {
2329
+ return await readSettlement(request, await this.credential({}), options);
2330
+ }
2331
+ /** Verify a delivery and read the payment out of it, null when it is not believable */
2332
+ async readPayment(request, options) {
2333
+ return await readPayment(request, await this.credential({}), options);
2334
+ }
2335
+ /** Answer the challenge the gateway sends before it will post to a webhook of yours */
2336
+ async answerWebhookChallenge(request, options) {
2337
+ return await answerWebhookChallenge(request, await this.credential({}), options);
2338
+ }
2339
+ async credential(handlers) {
2340
+ return handlers.credential ?? { publicKey: await this.gateway.webhookKey() };
2341
+ }
2342
+ };
2343
+ async function acted(settlement, handlers) {
2344
+ if (carriesProof(settlement)) {
2345
+ await handlers.onSettled?.(settlement);
2346
+ return new Response("ok");
2347
+ }
2348
+ if (handlers.onUnproven === void 0) {
2349
+ return new Response("the recipient released no preimage", { status: 202 });
2350
+ }
2351
+ await handlers.onUnproven(settlement);
2352
+ return new Response("ok");
2353
+ }
2354
+
1467
2355
  // src/client.ts
1468
2356
  var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
1469
2357
  var RECONNECT_DELAY_MS = 3e3;
@@ -1483,6 +2371,13 @@ function backoffMs(firstDelay, attempt) {
1483
2371
  const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
1484
2372
  return Math.round(grown * (0.5 + Math.random() / 2));
1485
2373
  }
2374
+ function gatewayAt(baseUrl) {
2375
+ const at = URL.canParse(baseUrl) ? new URL(baseUrl) : null;
2376
+ if (at === null || at.protocol !== "http:" && at.protocol !== "https:") {
2377
+ throw new Error(`${baseUrl} is not an http or https gateway url`);
2378
+ }
2379
+ return baseUrl.replace(/\/+$/, "");
2380
+ }
1486
2381
  var ThunderBridge = class {
1487
2382
  baseUrl;
1488
2383
  verify;
@@ -1490,28 +2385,34 @@ var ThunderBridge = class {
1490
2385
  secret;
1491
2386
  strangers = null;
1492
2387
  speaks = null;
2388
+ published = null;
2389
+ /** Everything this gateway lets you mount, from an LNURL endpoint to a webhook route */
2390
+ serve;
2391
+ /** One call per sale, whatever the rail moves */
2392
+ rails;
1493
2393
  constructor(baseUrl, options) {
1494
- this.baseUrl = baseUrl.replace(/\/+$/, "");
2394
+ this.baseUrl = gatewayAt(baseUrl);
1495
2395
  this.verify = options?.verify ?? true;
1496
2396
  this.token = options?.token ?? null;
1497
2397
  this.secret = options?.secret ?? null;
2398
+ this.serve = new Serve(this);
2399
+ this.rails = new Rails(this);
1498
2400
  }
1499
2401
  /**
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
2402
+ * Whether a token was given to this instance, which is your side of the
2403
+ * arrangement and says nothing about the gateway's. `refusesStrangers` is the
2404
+ * one that asks the gateway, and it is the one to guard anything with
1503
2405
  */
1504
- get isPrivate() {
2406
+ get hasToken() {
1505
2407
  return this.token !== null;
1506
2408
  }
1507
2409
  /**
1508
2410
  * 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
2411
+ * one unauthenticated read it would have to refuse. `hasToken` answers only
2412
+ * whether you configured one, so a made-up token against a public instance
2413
+ * reads as yours and is not. Asked once and remembered, because an instance
2414
+ * does not change its mind. Anything other than a refusal counts as open, so
2415
+ * an unreachable gateway fails closed
1515
2416
  */
1516
2417
  async refusesStrangers() {
1517
2418
  this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
@@ -1519,14 +2420,24 @@ var ThunderBridge = class {
1519
2420
  }).then((answer) => answer.status === 401).catch(() => false);
1520
2421
  return await this.strangers;
1521
2422
  }
2423
+ /**
2424
+ * Ask to be paid for one thing. It mints the invoice, proves it came from the
2425
+ * address you asked for, draws the QR and hands back one object with a way to
2426
+ * wait for the money. This is `mint` plus the two things every caller does next
2427
+ */
2428
+ async requestPayment(asked) {
2429
+ return paymentRequestOf(this, await this.mint(asked, asked), asked);
2430
+ }
1522
2431
  /**
1523
2432
  * Ask the gateway for an invoice payable to the first address on your list
1524
2433
  * that can issue a provable one, throws `NoWalletAvailableError` when none can
1525
2434
  * and `GatewayCheatError` when what comes back is not what you asked for
1526
2435
  */
1527
- async createPayment(params, options) {
2436
+ async mint(charge, options) {
2437
+ const asked = await priced(charge);
1528
2438
  const sent = createRequestBody(
1529
- params,
2439
+ asked,
2440
+ charge.webhookUrl,
1530
2441
  options?.trigger ? sha256Hex(unguessable(options.trigger)) : null,
1531
2442
  options?.replay
1532
2443
  );
@@ -1542,9 +2453,9 @@ var ThunderBridge = class {
1542
2453
  if (!response.ok) {
1543
2454
  throw await problemFrom(response);
1544
2455
  }
1545
- const payment = await paymentFrom(response);
2456
+ const payment = await mintedFrom(response);
1546
2457
  if (this.verify) {
1547
- await proveOrigin(payment, params);
2458
+ await proveOrigin(payment, asked);
1548
2459
  }
1549
2460
  return payment;
1550
2461
  }
@@ -1555,8 +2466,8 @@ var ThunderBridge = class {
1555
2466
  * whether a wallet returns a provable invoice cannot be known without asking
1556
2467
  * it for one, and asking mints it
1557
2468
  */
1558
- async createQuote(params) {
1559
- const sent = quoteRequestBody(params);
2469
+ async quote(charge) {
2470
+ const sent = quoteRequestBody(await priced(charge));
1560
2471
  const response = await fetch(`${this.baseUrl}/quotes`, {
1561
2472
  method: "POST",
1562
2473
  headers: await this.sending("/quotes", sent),
@@ -1574,13 +2485,19 @@ var ThunderBridge = class {
1574
2485
  }
1575
2486
  return quote;
1576
2487
  }
1577
- /** Read a payment back, null when the gateway has never heard of it */
1578
2488
  /**
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
2489
+ * The key this gateway signs webhooks with when you registered none of your own.
2490
+ * `serve.webhook` reads it for you. Asked once and kept, because it is the same
2491
+ * for every instance in the cluster
1582
2492
  */
1583
2493
  async webhookKey() {
2494
+ this.published ??= this.publishedKey().catch((refused) => {
2495
+ this.published = null;
2496
+ throw refused;
2497
+ });
2498
+ return await this.published;
2499
+ }
2500
+ async publishedKey() {
1584
2501
  const response = await fetch(`${this.baseUrl}/webhook-key`);
1585
2502
  if (!response.ok) {
1586
2503
  throw await problemFrom(response);
@@ -1594,25 +2511,12 @@ var ThunderBridge = class {
1594
2511
  }
1595
2512
  return body.public_key;
1596
2513
  }
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
2514
  /**
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
2515
+ * Read a payment back, null when the gateway has never heard of it. One method
2516
+ * for both sorts: `kind` says whether the gateway minted it or was handed it,
2517
+ * and the address, amount and invoice are null on one it was never told
1614
2518
  */
1615
- async getWatched(id) {
2519
+ async payment(id) {
1616
2520
  const path = `/incoming-payments/${encodeURIComponent(id)}`;
1617
2521
  const response = await fetch(`${this.baseUrl}${path}`, {
1618
2522
  headers: await this.reading("GET", path)
@@ -1620,17 +2524,10 @@ var ThunderBridge = class {
1620
2524
  if (response.status === 404) {
1621
2525
  return null;
1622
2526
  }
1623
- if (!response.ok) {
1624
- throw await problemFrom(response);
1625
- }
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
- });
2527
+ if (!response.ok) {
2528
+ throw await problemFrom(response);
1632
2529
  }
1633
- return this.proven(watched);
2530
+ return this.proven(await paymentFrom(response));
1634
2531
  }
1635
2532
  /**
1636
2533
  * List what this gateway is watching, newest first. Only a gateway started
@@ -1641,7 +2538,7 @@ var ThunderBridge = class {
1641
2538
  * Anything older than that window is not in the answer, and the list does not
1642
2539
  * pretend otherwise
1643
2540
  */
1644
- async listPayments(limit) {
2541
+ async payments(limit) {
1645
2542
  const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
1646
2543
  const response = await fetch(`${this.baseUrl}${path}`, {
1647
2544
  headers: await this.reading("GET", path)
@@ -1650,7 +2547,7 @@ var ThunderBridge = class {
1650
2547
  throw await problemFrom(response);
1651
2548
  }
1652
2549
  const body = await response.json().catch(() => null);
1653
- const listed = Array.isArray(body?.payments) ? body.payments.map(triggerEventFromWire).filter((one) => one !== null) : null;
2550
+ const listed = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null) : null;
1654
2551
  if (listed === null || typeof body?.settled_scanned !== "number") {
1655
2552
  throw new ProblemError({
1656
2553
  status: response.status,
@@ -1665,34 +2562,15 @@ var ThunderBridge = class {
1665
2562
  * one that has answered is followed until its own expiry, so the wait always
1666
2563
  * ends by itself
1667
2564
  */
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) {
2565
+ async settled(id, options) {
2566
+ const ended = paymentFromWire(await this.followed(id, options));
2567
+ if (ended === null) {
1690
2568
  throw new ProblemError({
1691
2569
  status: 200,
1692
2570
  title: "The gateway answered with something that is not a payment"
1693
2571
  });
1694
2572
  }
1695
- return this.proven(watched);
2573
+ return this.proven(ended);
1696
2574
  }
1697
2575
  followed(id, options) {
1698
2576
  const base = this.baseUrl.replace(/^http/, "ws");
@@ -1759,7 +2637,7 @@ var ThunderBridge = class {
1759
2637
  opened.onmessage = (event) => {
1760
2638
  try {
1761
2639
  const frame = JSON.parse(String(event.data));
1762
- const watched = triggerEventFromWire(frame);
2640
+ const watched = paymentFromWire(frame);
1763
2641
  if (watched === null) {
1764
2642
  return;
1765
2643
  }
@@ -1788,16 +2666,16 @@ var ThunderBridge = class {
1788
2666
  * what you want is the one that arrives and nothing further from the other.
1789
2667
  *
1790
2668
  * 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
2669
+ * not a race: `settled` ends on `paid` and on `expired` alike, and a
1792
2670
  * Lightning invoice expires in an hour while a bank transfer takes days. `null`
1793
2671
  * means every leg ended without being paid.
1794
2672
  *
1795
2673
  * Stopping the wait is not revoking the invoice. Nobody can revoke one, because
1796
2674
  * 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.
2675
+ * really does pay twice and that shows up on `follow` as a second settlement to
2676
+ * refund.
1799
2677
  */
1800
- async firstToSettle(ids, options) {
2678
+ async firstSettled(ids, options) {
1801
2679
  if (ids.length === 0) {
1802
2680
  return null;
1803
2681
  }
@@ -1814,7 +2692,7 @@ var ThunderBridge = class {
1814
2692
  }
1815
2693
  };
1816
2694
  for (const id of ids) {
1817
- this.waitForWatched(id, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
2695
+ this.settled(id, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
1818
2696
  refused ??= failure;
1819
2697
  lost();
1820
2698
  });
@@ -1834,10 +2712,10 @@ var ThunderBridge = class {
1834
2712
  * rather than one recipient, which is what makes leaving it cheap. Anything
1835
2713
  * the watcher needs goes in `sealed`, which the gateway cannot read
1836
2714
  */
1837
- async watchPayment(params) {
2715
+ async watch(handover) {
1838
2716
  const sent = watchRequestBody(
1839
- params,
1840
- params.trigger ? sha256Hex(unguessable(params.trigger)) : null
2717
+ handover,
2718
+ handover.trigger ? sha256Hex(unguessable(handover.trigger)) : null
1841
2719
  );
1842
2720
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
1843
2721
  method: "POST",
@@ -1847,14 +2725,14 @@ var ThunderBridge = class {
1847
2725
  if (!response.ok) {
1848
2726
  throw await problemFrom(response);
1849
2727
  }
1850
- const watched = triggerEventFromWire(await response.json().catch(() => null));
2728
+ const watched = paymentFromWire(await response.json().catch(() => null));
1851
2729
  if (watched === null) {
1852
2730
  throw new ProblemError({
1853
2731
  status: response.status,
1854
2732
  title: "The gateway answered with something that is not a watched payment"
1855
2733
  });
1856
2734
  }
1857
- const named = await this.nameFor(params.paymentHash);
2735
+ const named = await this.nameFor(handover.paymentHash);
1858
2736
  if (named !== null && watched.id !== named) {
1859
2737
  throw new GatewayCheatError("id_not_mine", watched.id);
1860
2738
  }
@@ -1875,7 +2753,7 @@ var ThunderBridge = class {
1875
2753
  * connect and then live, reconnecting on its own until the returned function
1876
2754
  * is called. A trigger has no terminal state, so this never resolves
1877
2755
  */
1878
- followTrigger(secret, options) {
2756
+ follow(secret, options) {
1879
2757
  const base = this.baseUrl.replace(/^http/, "ws");
1880
2758
  const asked = options.replay === void 0 ? "" : `?replay=${options.replay}`;
1881
2759
  const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}${asked}`;
@@ -1914,7 +2792,7 @@ var ThunderBridge = class {
1914
2792
  };
1915
2793
  socket.onmessage = (event) => {
1916
2794
  try {
1917
- const settled = triggerEventFromWire(JSON.parse(String(event.data)));
2795
+ const settled = paymentFromWire(JSON.parse(String(event.data)));
1918
2796
  if (settled !== null) {
1919
2797
  options.onPayment(this.proven(settled));
1920
2798
  }
@@ -1936,13 +2814,13 @@ var ThunderBridge = class {
1936
2814
  * A one minute pass onto one trigger's stream, for something that must hold
1937
2815
  * neither the token nor the trigger secret. Mint it in a handler and answer
1938
2816
  * 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
2817
+ * all it can do anything with. `serve.watchTicket` is this method already
1940
2818
  * wrapped in a route
1941
2819
  */
1942
- async createSocketTicket(params) {
2820
+ async ticket(trigger, options) {
1943
2821
  return await this.mintedTicket({
1944
- trigger_secret: unguessable(params.trigger),
1945
- replay: params.replay
2822
+ trigger_secret: unguessable(trigger),
2823
+ replay: options?.replay
1946
2824
  });
1947
2825
  }
1948
2826
  needsTicket(asked) {
@@ -1983,15 +2861,8 @@ var ThunderBridge = class {
1983
2861
  this.speaks ??= callerKey(this.secret ?? "");
1984
2862
  return await this.speaks;
1985
2863
  }
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);
1990
- }
1991
- return settled;
1992
- }
1993
- checked(payment) {
1994
- if (this.verify && payment.status === "paid" && !isProvablyPaid(payment)) {
2864
+ proven(payment) {
2865
+ if (this.verify && payment.status === "paid" && !carriesProof(payment)) {
1995
2866
  throw new GatewayCheatError("preimage_mismatch", payment.id);
1996
2867
  }
1997
2868
  return payment;
@@ -2007,11 +2878,21 @@ async function paymentFrom(response) {
2007
2878
  }
2008
2879
  return payment;
2009
2880
  }
2881
+ async function mintedFrom(response) {
2882
+ const payment = mintedFromWire(await response.json().catch(() => null));
2883
+ if (payment === null) {
2884
+ throw new ProblemError({
2885
+ status: response.status,
2886
+ title: "The gateway answered with something that is not a minted payment"
2887
+ });
2888
+ }
2889
+ return payment;
2890
+ }
2010
2891
  async function problemFrom(response) {
2011
2892
  const problem = await response.json().catch(() => ({}));
2012
2893
  const document = { ...problem, status: response.status };
2013
2894
  if (isProblemType(problem, NO_WALLET_AVAILABLE)) {
2014
- return new NoWalletAvailableError(document, refusals(problem.wallets));
2895
+ return new NoWalletAvailableError(document, refusals2(problem.wallets));
2015
2896
  }
2016
2897
  if (isProblemType(problem, REQUEST_IN_FLIGHT)) {
2017
2898
  return new IdempotencyConflictError(document, "request-in-flight");
@@ -2021,7 +2902,7 @@ async function problemFrom(response) {
2021
2902
  }
2022
2903
  return new ProblemError(document);
2023
2904
  }
2024
- function refusals(wallets) {
2905
+ function refusals2(wallets) {
2025
2906
  if (!Array.isArray(wallets)) {
2026
2907
  return [];
2027
2908
  }
@@ -2030,118 +2911,6 @@ function refusals(wallets) {
2030
2911
  });
2031
2912
  }
2032
2913
 
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
2914
  // src/gateways.ts
2146
2915
  var Gateways = class {
2147
2916
  each;
@@ -2165,10 +2934,8 @@ var Gateways = class {
2165
2934
  * carrying the first refusal, because one gateway that agreed is enough to be
2166
2935
  * watched
2167
2936
  */
2168
- async watchPayment(params) {
2169
- const asked = await Promise.allSettled(
2170
- this.each.map((gateway) => gateway.watchPayment(params))
2171
- );
2937
+ async watch(handover) {
2938
+ const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watch(handover)));
2172
2939
  const taken = [];
2173
2940
  const refusals3 = [];
2174
2941
  for (const [at, answer] of asked.entries()) {
@@ -2190,7 +2957,7 @@ var Gateways = class {
2190
2957
  * saying so. When they all end without a payment, the first ending is the answer,
2191
2958
  * and when they all fail, the first failure is thrown
2192
2959
  */
2193
- async waitForWatched(id, options) {
2960
+ async settled(id, options) {
2194
2961
  const stopLosers = new AbortController();
2195
2962
  const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
2196
2963
  const unpaid = [];
@@ -2205,7 +2972,7 @@ var Gateways = class {
2205
2972
  }
2206
2973
  };
2207
2974
  for (const gateway of this.each) {
2208
- gateway.waitForWatched(id, { ...options, signal }).then((watched) => {
2975
+ gateway.settled(id, { ...options, signal }).then((watched) => {
2209
2976
  if (watched.status === "paid") {
2210
2977
  return resolve2(watched);
2211
2978
  }
@@ -2229,315 +2996,30 @@ var Gateways = class {
2229
2996
  }
2230
2997
  }
2231
2998
  };
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
2999
  // Annotate the CommonJS export names for ESM import in node:
2486
3000
  0 && (module.exports = {
3001
+ AmountError,
2487
3002
  GatewayCheatError,
2488
3003
  Gateways,
2489
- IDEMPOTENCY_KEY_REUSED,
2490
3004
  IdempotencyConflictError,
2491
- NO_WALLET_AVAILABLE,
2492
3005
  NoWalletAvailableError,
2493
- PAYMENT_ALREADY_WATCHED,
2494
3006
  ProblemError,
2495
- REQUEST_IN_FLIGHT,
2496
3007
  ThunderBridge,
2497
3008
  UnverifiedRecipientError,
2498
3009
  WrapRefusedError,
2499
3010
  answerVerifyChallenge,
2500
- answerVerifyChallengeRequest,
2501
- answerWebhookChallenge,
2502
- answerWebhookChallengeRequest,
2503
- bankRail,
2504
- bankTransfer,
2505
- bankVerifyEndpoint,
2506
- bitstamp,
2507
- coinbase,
2508
- coinmate,
3011
+ carriesProof,
2509
3012
  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,
3013
+ fiat,
3014
+ invoiceFrom,
3015
+ msat,
2530
3016
  preimageMatchesHash,
2531
3017
  proveOrigin,
2532
3018
  proveSettlement,
2533
3019
  proveWrapped,
2534
- qrToDataUrl,
2535
- qrToSvg,
3020
+ relayedVerifyUrl,
3021
+ sats,
2536
3022
  seal,
2537
- spdToDataUrl,
2538
- spdToSvg,
2539
- toLnurl,
2540
3023
  unseal,
2541
- verifyWebhookSignature,
2542
3024
  wrapFeeCeiling
2543
3025
  });