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