thunder-bridge 1.4.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,24 +15,12 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
- // src/server.ts
31
- var server_exports = {};
32
- __export(server_exports, {
20
+ // src/entry/nwc.ts
21
+ var nwc_exports = {};
22
+ __export(nwc_exports, {
33
23
  askWallet: () => askWallet,
34
- blindLightningRail: () => blindLightningRail,
35
- invoiceFrom: () => invoiceFrom,
36
- lightningVerifyEndpoint: () => lightningVerifyEndpoint,
37
- lnurlPayEndpoint: () => lnurlPayEndpoint,
38
24
  nwcConnection: () => nwcConnection,
39
25
  nwcHoldInvoice: () => nwcHoldInvoice,
40
26
  nwcInvoice: () => nwcInvoice,
@@ -42,12 +28,9 @@ __export(server_exports, {
42
28
  nwcRail: () => nwcRail,
43
29
  nwcSettlement: () => nwcSettlement,
44
30
  nwcVerifyEndpoint: () => nwcVerifyEndpoint,
45
- nwcVerifyUrl: () => nwcVerifyUrl,
46
- publicWatchTicketEndpoint: () => publicWatchTicketEndpoint,
47
- relayedVerifyUrl: () => relayedVerifyUrl,
48
- watchTicketEndpoint: () => watchTicketEndpoint
31
+ nwcVerifyUrl: () => nwcVerifyUrl
49
32
  });
50
- module.exports = __toCommonJS(server_exports);
33
+ module.exports = __toCommonJS(nwc_exports);
51
34
 
52
35
  // ../core/bytes.ts
53
36
  function bytesToHex(bytes) {
@@ -64,8 +47,8 @@ function hexToBytes(hex) {
64
47
  }
65
48
  return bytes;
66
49
  }
67
- function isHex(text2) {
68
- return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
50
+ function isHex(text) {
51
+ return /^[0-9a-f]+$/i.test(text) && text.length % 2 === 0;
69
52
  }
70
53
 
71
54
  // ../core/sha256.ts
@@ -159,9 +142,6 @@ function sha256(bytes) {
159
142
  }
160
143
  return asBytes(state);
161
144
  }
162
- function sha256Hex(text2) {
163
- return bytesToHex(sha256(new TextEncoder().encode(text2)));
164
- }
165
145
  function withPadding(bytes) {
166
146
  const padded2 = new Uint8Array(
167
147
  bytes.length + 1 + LENGTH_BYTES + BLOCK_BYTES - 1 & ~(BLOCK_BYTES - 1)
@@ -226,6 +206,7 @@ function rotate(value, by) {
226
206
 
227
207
  // ../core/bolt11.ts
228
208
  var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
209
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
229
210
  var CHECKSUM_WORDS = 6;
230
211
  var TIMESTAMP_WORDS = 7;
231
212
  var SIGNATURE_WORDS = 104;
@@ -243,18 +224,27 @@ var HRP_MULTIPLIER_MSAT = {
243
224
  n: MSAT_PER_BTC / 1e9,
244
225
  p: MSAT_PER_BTC / 1e12
245
226
  };
246
- var HRP_AMOUNT = /^ln[a-z]+?(\d+)([munp]?)$/;
227
+ var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
247
228
  function decodeInvoice(bolt11) {
248
229
  const parts = splitBech32(bolt11);
249
230
  if (parts === null) {
250
- return { paymentHash: null, descriptionHash: null, amountMsat: null, expiresAt: null };
231
+ return {
232
+ paymentHash: null,
233
+ descriptionHash: null,
234
+ amountMsat: null,
235
+ issuedAt: null,
236
+ expiresAt: null
237
+ };
251
238
  }
252
239
  const tagged = taggedFields(parts.words);
240
+ const issuedAt = issuedAtOf(parts.words);
241
+ const expiry = tagged.get(EXPIRY_TAG);
253
242
  return {
254
243
  paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
255
244
  descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
256
245
  amountMsat: amountFromHrp(parts.hrp),
257
- expiresAt: expiryOf(parts.words, tagged)
246
+ issuedAt,
247
+ expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
258
248
  };
259
249
  }
260
250
  function preimageMatchesHash(preimage, paymentHash) {
@@ -265,7 +255,7 @@ function preimageMatchesHash(preimage, paymentHash) {
265
255
  }
266
256
  function splitBech32(bolt11) {
267
257
  const lower = bolt11.toLowerCase();
268
- if (isBolt12Encoding(lower)) {
258
+ if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
269
259
  return null;
270
260
  }
271
261
  const separator = lower.lastIndexOf("1");
@@ -284,7 +274,28 @@ function splitBech32(bolt11) {
284
274
  }
285
275
  words.push(value);
286
276
  }
287
- return { hrp: lower.slice(0, separator), words: words.slice(0, -CHECKSUM_WORDS) };
277
+ const hrp = lower.slice(0, separator);
278
+ if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
279
+ return null;
280
+ }
281
+ return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
282
+ }
283
+ function expandedHrp(hrp) {
284
+ const codes = [...hrp].map((char) => char.charCodeAt(0));
285
+ return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
286
+ }
287
+ function bech32Polymod(values) {
288
+ let checksum = 1;
289
+ for (const value of values) {
290
+ const top = checksum >> 25;
291
+ checksum = (checksum & 33554431) << 5 ^ value;
292
+ for (let bit = 0; bit < 5; bit++) {
293
+ if (top >> bit & 1) {
294
+ checksum ^= BECH32_GENERATOR[bit];
295
+ }
296
+ }
297
+ }
298
+ return checksum;
288
299
  }
289
300
  function isBolt12Encoding(lower) {
290
301
  return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
@@ -314,19 +325,17 @@ function hexTag(fields, tag, expectedWords) {
314
325
  }
315
326
  return bytesToHex(wordsToBytes(words));
316
327
  }
317
- function expiryOf(words, fields) {
328
+ function issuedAtOf(words) {
318
329
  if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
319
330
  return null;
320
331
  }
321
- const expiry = fields.get(EXPIRY_TAG);
322
- return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
332
+ return readNumber(words.slice(0, TIMESTAMP_WORDS));
323
333
  }
324
334
  function amountFromHrp(hrp) {
325
- const match = HRP_AMOUNT.exec(hrp);
326
- if (match === null) {
335
+ const [, digits, multiplier] = HRP.exec(hrp) ?? [];
336
+ if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
327
337
  return null;
328
338
  }
329
- const [, digits, multiplier] = match;
330
339
  return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
331
340
  }
332
341
  function readNumber(words) {
@@ -355,36 +364,40 @@ var WalletRefused = class extends Error {
355
364
  this.reason = reason;
356
365
  }
357
366
  };
358
- var NoWalletAvailable = class extends Error {
359
- wallets;
360
- constructor(wallets) {
361
- super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
362
- this.wallets = wallets;
363
- }
364
- };
365
367
 
366
368
  // ../core/sealed.ts
367
- var VERSION = "v1";
369
+ var VERSION = "v2";
370
+ var UNPADDED_VERSION = "v1";
368
371
  var IV_BYTES = 12;
369
372
  var MIN_SECRET_CHARS = 32;
370
373
  var MAX_PLAIN_BYTES = 3e3;
374
+ var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
375
+ var END_OF_TEXT = 128;
371
376
  var INFO = new TextEncoder().encode("thunder-bridge/sealed");
372
- async function seal(secret, plaintext) {
373
- const body = new TextEncoder().encode(plaintext);
374
- if (body.length > MAX_PLAIN_BYTES) {
375
- throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${body.length}`);
376
- }
377
+ async function seal(secret, plaintext, paymentHash) {
378
+ return await sealUnder(
379
+ secret,
380
+ plaintext,
381
+ crypto.getRandomValues(new Uint8Array(IV_BYTES)),
382
+ boundTo(paymentHash)
383
+ );
384
+ }
385
+ async function sealUnder(secret, plaintext, iv, additionalData) {
386
+ const body = padToClass(plaintext);
377
387
  const key = await keyFor(secret);
378
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
379
- const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, body));
388
+ const cipher = new Uint8Array(
389
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
390
+ );
380
391
  const joined2 = new Uint8Array(iv.length + cipher.length);
381
392
  joined2.set(iv);
382
393
  joined2.set(cipher, iv.length);
383
394
  return `${VERSION}.${toBase64Url(joined2)}`;
384
395
  }
385
- async function unseal(secret, sealed) {
396
+ async function unseal(secret, sealed, paymentHash) {
386
397
  const key = await keyFor(secret);
387
- if (!sealed.startsWith(`${VERSION}.`)) {
398
+ const padded2 = sealed.startsWith(`${VERSION}.`);
399
+ const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
400
+ if (!padded2 && (!unbound || paymentHash !== void 0)) {
388
401
  return null;
389
402
  }
390
403
  const joined2 = fromBase64Url(sealed.slice(VERSION.length + 1));
@@ -392,20 +405,50 @@ async function unseal(secret, sealed) {
392
405
  return null;
393
406
  }
394
407
  try {
395
- const body = await crypto.subtle.decrypt(
396
- { name: "AES-GCM", iv: joined2.slice(0, IV_BYTES) },
397
- key,
398
- joined2.slice(IV_BYTES)
408
+ const body = new Uint8Array(
409
+ await crypto.subtle.decrypt(
410
+ {
411
+ name: "AES-GCM",
412
+ iv: joined2.slice(0, IV_BYTES),
413
+ additionalData: padded2 ? boundTo(paymentHash) : void 0
414
+ },
415
+ key,
416
+ joined2.slice(IV_BYTES)
417
+ )
399
418
  );
400
- return new TextDecoder().decode(body);
419
+ const text = padded2 ? unpad(body) : body;
420
+ return text === null ? null : new TextDecoder().decode(text);
401
421
  } catch {
402
422
  return null;
403
423
  }
404
424
  }
405
- async function keyFor(secret) {
425
+ function refuseAWeakSecret(secret) {
406
426
  if (secret.length < MIN_SECRET_CHARS) {
407
427
  throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
408
428
  }
429
+ }
430
+ function boundTo(paymentHash) {
431
+ return new TextEncoder().encode(
432
+ paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
433
+ );
434
+ }
435
+ function padToClass(plaintext) {
436
+ const text = new TextEncoder().encode(plaintext);
437
+ const size = SIZE_CLASSES.find((limit) => text.length <= limit);
438
+ if (size === void 0) {
439
+ throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text.length}`);
440
+ }
441
+ const body = new Uint8Array(size + 1);
442
+ body.set(text);
443
+ body[text.length] = END_OF_TEXT;
444
+ return body;
445
+ }
446
+ function unpad(body) {
447
+ const end = body.lastIndexOf(END_OF_TEXT);
448
+ return end === -1 ? null : body.subarray(0, end);
449
+ }
450
+ async function keyFor(secret) {
451
+ refuseAWeakSecret(secret);
409
452
  const material = await crypto.subtle.importKey(
410
453
  "raw",
411
454
  new TextEncoder().encode(secret),
@@ -428,12 +471,12 @@ function toBase64Url(bytes) {
428
471
  }
429
472
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
430
473
  }
431
- function fromBase64Url(text2) {
432
- if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) {
474
+ function fromBase64Url(text) {
475
+ if (text.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text)) {
433
476
  return null;
434
477
  }
435
478
  try {
436
- const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
479
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
437
480
  const bytes = new Uint8Array(binary.length);
438
481
  for (let at = 0; at < bytes.length; at++) {
439
482
  bytes[at] = binary.charCodeAt(at);
@@ -448,7 +491,7 @@ function fromBase64Url(text2) {
448
491
  var import_chacha = require("@noble/ciphers/chacha.js");
449
492
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
450
493
  var import_hkdf = require("@noble/hashes/hkdf.js");
451
- var import_hmac = require("@noble/hashes/hmac.js");
494
+ var import_hmac2 = require("@noble/hashes/hmac.js");
452
495
  var import_sha2 = require("@noble/hashes/sha2.js");
453
496
  var NIP44_SALT = new TextEncoder().encode("nip44-v2");
454
497
  var NIP44_VERSION = 2;
@@ -506,7 +549,7 @@ function encryptNip44(conversationKey, plaintext) {
506
549
  function sealNip44(conversationKey, plaintext, nonce) {
507
550
  const keys = messageKeysFrom(conversationKey, nonce);
508
551
  const ciphertext = (0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, padded(plaintext));
509
- const mac = (0, import_hmac.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext));
552
+ const mac = (0, import_hmac2.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext));
510
553
  return toBase64(joined(Uint8Array.of(NIP44_VERSION), nonce, ciphertext, mac));
511
554
  }
512
555
  function decryptNip44(conversationKey, payload) {
@@ -524,7 +567,7 @@ function decryptNip44(conversationKey, payload) {
524
567
  const ciphertext = raw.subarray(1 + NONCE_BYTES, raw.length - MAC_BYTES);
525
568
  const mac = raw.subarray(raw.length - MAC_BYTES);
526
569
  const keys = messageKeysFrom(conversationKey, nonce);
527
- if (!sameBytes((0, import_hmac.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext)), mac)) {
570
+ if (!sameBytes((0, import_hmac2.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext)), mac)) {
528
571
  throw new Error("nip44 payload failed its mac, so it was not written with this key");
529
572
  }
530
573
  return unpadded((0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, ciphertext));
@@ -590,8 +633,8 @@ function toBase64(bytes) {
590
633
  }
591
634
  return btoa(binary);
592
635
  }
593
- function fromBase64(text2) {
594
- const binary = atob(text2);
636
+ function fromBase64(text) {
637
+ const binary = atob(text);
595
638
  const bytes = new Uint8Array(binary.length);
596
639
  for (let at = 0; at < bytes.length; at++) {
597
640
  bytes[at] = binary.charCodeAt(at);
@@ -599,14 +642,257 @@ function fromBase64(text2) {
599
642
  return bytes;
600
643
  }
601
644
 
645
+ // src/qr.ts
646
+ var import_uqr = require("uqr");
647
+ function toLightningUri(destination) {
648
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
649
+ return `LIGHTNING:${scanned}`;
650
+ }
651
+ function isLnAddress(destination) {
652
+ const at = destination.indexOf("@");
653
+ return at > 0 && destination.slice(at + 1).includes(".");
654
+ }
655
+
656
+ // src/errors.ts
657
+ var AmountError = class extends Error {
658
+ /**
659
+ * Whether a failure is one of these, without asking whether it is this exact
660
+ * class. Every entry point carries its own copy of the class, so a price
661
+ * refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
662
+ * imported from `thunder-bridge`. The name and the code are the same in every
663
+ * copy, so this holds where `instanceof` does not
664
+ */
665
+ static is(failure) {
666
+ return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
667
+ }
668
+ code;
669
+ constructor(code, detail) {
670
+ super(`${code}: ${detail}`);
671
+ this.name = "AmountError";
672
+ this.code = code;
673
+ }
674
+ };
675
+
676
+ // src/currency.ts
677
+ var ISO_4217_MINOR_UNITS = {
678
+ BHD: 3,
679
+ BIF: 0,
680
+ CLP: 0,
681
+ CZK: 2,
682
+ DJF: 0,
683
+ EUR: 2,
684
+ GBP: 2,
685
+ ISK: 0,
686
+ IQD: 3,
687
+ JOD: 3,
688
+ JPY: 0,
689
+ KRW: 0,
690
+ KWD: 3,
691
+ LYD: 3,
692
+ OMR: 3,
693
+ PLN: 2,
694
+ PYG: 0,
695
+ RWF: 0,
696
+ TND: 3,
697
+ UGX: 0,
698
+ USD: 2,
699
+ VND: 0,
700
+ VUV: 0,
701
+ XAF: 0,
702
+ XOF: 0,
703
+ XPF: 0
704
+ };
705
+ function minorUnitsOf(currency) {
706
+ const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
707
+ if (digits === void 0) {
708
+ throw new AmountError(
709
+ "unknown-currency",
710
+ `${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
711
+ );
712
+ }
713
+ return digits;
714
+ }
715
+ function minorScaleOf(currency) {
716
+ return 10 ** minorUnitsOf(currency);
717
+ }
718
+
719
+ // src/price.ts
720
+ var MSAT_PER_BTC2 = 1e11;
721
+ var BASIS_POINTS = 1e4;
722
+ var DEFAULT_HOLD_SECS = 60;
723
+ var DEFAULT_MIN_VENUES = 2;
724
+ var DEFAULT_MAX_SPREAD_BPS = 200;
725
+ var MILLIS = 1e3;
726
+ function coinbase(baseUrl = "https://api.coinbase.com") {
727
+ return async (currency) => {
728
+ const asked = currency.toUpperCase();
729
+ const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
730
+ const quoted = answer.data;
731
+ if (quoted?.currency !== asked) {
732
+ throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
733
+ }
734
+ return asMinor(quoted.amount, "coinbase", asked);
735
+ };
736
+ }
737
+ function kraken(baseUrl = "https://api.kraken.com") {
738
+ return async (currency) => {
739
+ const asked = currency.toUpperCase();
740
+ const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
741
+ if (answer.error?.length) {
742
+ throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
743
+ }
744
+ const pairs = Object.values(answer.result ?? {});
745
+ if (pairs.length !== 1) {
746
+ throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
747
+ }
748
+ return asMinor(pairs[0]?.c?.[0], "kraken", asked);
749
+ };
750
+ }
751
+ function bitstamp(baseUrl = "https://www.bitstamp.net") {
752
+ return async (currency) => {
753
+ const pair = `btc${currency.toLowerCase()}`;
754
+ const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
755
+ if (Array.isArray(answer)) {
756
+ throw new Error(`bitstamp has no ${pair} pair`);
757
+ }
758
+ return asMinor(answer.last, "bitstamp", currency);
759
+ };
760
+ }
761
+ function coinmate(baseUrl = "https://coinmate.io") {
762
+ return async (currency) => {
763
+ const pair = `BTC_${currency.toUpperCase()}`;
764
+ const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
765
+ if (answer.error !== false) {
766
+ throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
767
+ }
768
+ return asMinor(answer.data?.last, "coinmate", currency);
769
+ };
770
+ }
771
+ function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
772
+ const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
773
+ const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
774
+ const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
775
+ const held = /* @__PURE__ */ new Map();
776
+ return async (currency) => {
777
+ const asked = currency.toUpperCase();
778
+ const remembered = held.get(asked);
779
+ if (remembered && Date.now() - remembered.at < holdFor) {
780
+ return remembered.price;
781
+ }
782
+ const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
783
+ const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
784
+ if (quoted.length < minVenues) {
785
+ throw new Error(
786
+ `${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
787
+ );
788
+ }
789
+ const spread = spreadBpsOf(quoted);
790
+ if (spread > maxSpread) {
791
+ throw new Error(
792
+ `venues disagree on ${asked} by ${Math.round(spread)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
793
+ );
794
+ }
795
+ const price = middleOf(quoted);
796
+ held.set(asked, { at: Date.now(), price });
797
+ return price;
798
+ };
799
+ }
800
+ function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
801
+ if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
802
+ throw new Error("amountMinor must be a whole number of minor units above zero");
803
+ }
804
+ if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
805
+ throw new Error(`${priceMinorPerBtc} is not a usable price`);
806
+ }
807
+ const spreadBps = options.spreadBps ?? 0;
808
+ if (!Number.isInteger(spreadBps) || spreadBps < 0) {
809
+ throw new Error("spreadBps must be a whole number of basis points, none or more");
810
+ }
811
+ const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
812
+ const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
813
+ const msat2 = (wanted + per - 1n) / per;
814
+ if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
815
+ throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
816
+ }
817
+ return Number(msat2);
818
+ }
819
+ async function asJson(url, venue) {
820
+ const answer = await fetch(url, { headers: { accept: "application/json" } });
821
+ if (!answer.ok) {
822
+ throw new Error(`${venue} answered ${answer.status}`);
823
+ }
824
+ return answer.json();
825
+ }
826
+ function asMinor(quoted, venue, currency) {
827
+ const major = Number(quoted);
828
+ if (!Number.isFinite(major) || major <= 0) {
829
+ throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
830
+ }
831
+ return Math.round(major * minorScaleOf(currency));
832
+ }
833
+ function spreadBpsOf(sorted) {
834
+ const lowest = sorted[0] ?? 0;
835
+ const highest = sorted[sorted.length - 1] ?? 0;
836
+ return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
837
+ }
838
+ function middleOf(sorted) {
839
+ const middle = Math.floor(sorted.length / 2);
840
+ if (sorted.length % 2 === 1) {
841
+ return sorted[middle] ?? 0;
842
+ }
843
+ return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
844
+ }
845
+ function refusals(answers) {
846
+ return answers.filter((answer) => answer.status === "rejected").map(
847
+ (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
848
+ ).join("; ");
849
+ }
850
+
851
+ // src/amount.ts
852
+ var MSAT_PER_SAT = 1e3;
853
+ var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
854
+ function msat(exact) {
855
+ if (!Number.isSafeInteger(exact) || exact < 1) {
856
+ throw new AmountError(
857
+ "not-whole-millisatoshi",
858
+ `${exact} is not a payable count of millisatoshi`
859
+ );
860
+ }
861
+ return exact;
862
+ }
863
+ async function amountNow(amount) {
864
+ return typeof amount === "function" ? msat(await amount()) : msat(amount);
865
+ }
866
+
602
867
  // ../core/ed25519.ts
603
868
  var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
869
+ var SMALL_ORDER = [
870
+ "00".repeat(32),
871
+ `01${"00".repeat(31)}`,
872
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
873
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
874
+ `ec${"ff".repeat(30)}7f`,
875
+ `ed${"ff".repeat(30)}7f`,
876
+ `ee${"ff".repeat(30)}7f`
877
+ ];
604
878
 
605
879
  // src/webhook.ts
606
880
  var VERIFY_CHALLENGE = "verify-challenge";
607
- function nonceOf(text2, type) {
881
+ async function answerVerifyChallenge(request) {
882
+ if (request.method !== "POST") {
883
+ return null;
884
+ }
885
+ const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
886
+ return nonce === null ? null : answered(nonce);
887
+ }
888
+ function answered(nonce) {
889
+ return new Response(JSON.stringify({ nonce }), {
890
+ headers: { "content-type": "application/json" }
891
+ });
892
+ }
893
+ function nonceOf(text, type) {
608
894
  try {
609
- const said = JSON.parse(text2);
895
+ const said = JSON.parse(text);
610
896
  if (said["type"] !== type || typeof said["nonce"] !== "string") {
611
897
  return null;
612
898
  }
@@ -615,16 +901,13 @@ function nonceOf(text2, type) {
615
901
  return null;
616
902
  }
617
903
  }
618
- function answerVerifyChallenge(body) {
619
- const nonce = nonceOf(body, VERIFY_CHALLENGE);
620
- return nonce === null ? null : JSON.stringify({ nonce });
621
- }
622
- async function answerVerifyChallengeRequest(request) {
623
- if (request.method !== "POST") {
624
- return null;
904
+
905
+ // src/rail.ts
906
+ async function msatForOrder(order, amount, rate) {
907
+ if (amount !== void 0) {
908
+ return await amountNow(amount(order));
625
909
  }
626
- const answer = answerVerifyChallenge(await request.clone().text());
627
- return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
910
+ return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
628
911
  }
629
912
 
630
913
  // src/nwc.ts
@@ -745,7 +1028,7 @@ function nwcVerifyEndpoint(config) {
745
1028
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
746
1029
  };
747
1030
  return async (request) => {
748
- const consented = await answerVerifyChallengeRequest(request);
1031
+ const consented = await answerVerifyChallenge(request);
749
1032
  if (consented !== null) {
750
1033
  return consented;
751
1034
  }
@@ -905,505 +1188,14 @@ function isSecureRelay(relay) {
905
1188
  return false;
906
1189
  }
907
1190
  }
908
-
909
- // ../core/url.ts
910
- var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
911
- function publicHttps(raw) {
912
- let url;
913
- try {
914
- url = new URL(raw);
915
- } catch {
916
- return false;
917
- }
918
- return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
919
- }
920
- function sameOrigin(one, other) {
921
- try {
922
- return new URL(one).origin === new URL(other).origin;
923
- } catch {
924
- return false;
925
- }
926
- }
927
- function publicAddress(literal) {
928
- return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
929
- }
930
- function hostIsPublic(host) {
931
- if (host.startsWith("[")) {
932
- return publicAddress(host.slice(1, -1));
933
- }
934
- const name = host.replace(/\.$/, "");
935
- if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
936
- return publicAddress(name);
937
- }
938
- const labels = name.split(".");
939
- if (labels.length < 2 || labels.some((label) => label === "")) {
940
- return false;
941
- }
942
- return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
943
- }
944
- function ipv4IsGlobal(literal) {
945
- const parts = literal.split(".").map(Number);
946
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
947
- return false;
948
- }
949
- const [a, b] = parts;
950
- if (a === 0 || a === 10 || a === 127 || a >= 240) {
951
- return false;
952
- }
953
- if (a === 100 && b >= 64 && b < 128) {
954
- return false;
955
- }
956
- if (a === 169 && b === 254) {
957
- return false;
958
- }
959
- if (a === 172 && b >= 16 && b < 32) {
960
- return false;
961
- }
962
- if (a === 192 && b === 168) {
963
- return false;
964
- }
965
- return true;
966
- }
967
- function ipv6IsGlobal(literal) {
968
- const groups = ipv6Groups(literal.toLowerCase());
969
- if (groups.length !== 8 || groups.some(Number.isNaN)) {
970
- return false;
971
- }
972
- const leading = groups.slice(0, 5).every((group) => group === 0);
973
- if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
974
- return false;
975
- }
976
- if (leading && groups[5] === 65535) {
977
- const [high, low] = [groups[6], groups[7]];
978
- return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
979
- }
980
- const first = groups[0];
981
- if ((first & 65024) === 64512) {
982
- return false;
983
- }
984
- if ((first & 65472) === 65152) {
985
- return false;
986
- }
987
- return true;
988
- }
989
- function ipv6Groups(address) {
990
- const [head = "", tail] = address.split("::");
991
- const left = head.split(":").filter((group) => group !== "");
992
- const right = (tail ?? "").split(":").filter((group) => group !== "");
993
- const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
994
- return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
995
- }
996
-
997
- // ../core/outbound.ts
998
- var HTTP_TIMEOUT_MS = 15e3;
999
- var LOOKUP_TIMEOUT_MS = 5e3;
1000
- var HOPS_FOLLOWED = 2;
1001
- var REDIRECTS = [301, 302, 303, 307, 308];
1002
- var KEEPS_THE_METHOD = [307, 308];
1003
- var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
1004
- var BODY_LIMIT_BYTES = 262144;
1005
- var throughFetch = (url, sent, signal) => fetch(url, {
1006
- method: sent.method ?? "GET",
1007
- headers: sent.headers ?? {},
1008
- body: sent.body,
1009
- redirect: "manual",
1010
- signal
1011
- });
1012
- var send = throughFetch;
1013
- async function ask(url, sent = {}) {
1014
- const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
1015
- const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
1016
- let target = url;
1017
- let carried = sent;
1018
- for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
1019
- const at = await addressesToReach(target);
1020
- const response = await send(target, carried, signal, at);
1021
- if (!REDIRECTS.includes(response.status)) {
1022
- return await answerOf(response);
1023
- }
1024
- const location = response.headers.get("location");
1025
- if (location === null) {
1026
- throw new Error(`${target} answered ${response.status} without saying where to`);
1027
- }
1028
- await response.body?.cancel();
1029
- const next = new URL(location, target).toString();
1030
- carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
1031
- carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
1032
- target = next;
1033
- }
1034
- throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
1035
- }
1036
- async function addressesToReach(url) {
1037
- if (!publicHttps(url)) {
1038
- throw new Error(`${url} is not a public https URL`);
1039
- }
1040
- const found = await resolved(url);
1041
- if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
1042
- throw new Error(`${url} resolves to an address we do not reach`);
1043
- }
1044
- return found;
1045
- }
1046
- async function resolved(url) {
1047
- const { lookup } = await import("dns/promises");
1048
- const { setTimeout: sleep } = await import("timers/promises");
1049
- const host = new URL(url).hostname.replace(/^\[|]$/g, "");
1050
- const found = await Promise.race([
1051
- lookup(host, { all: true, verbatim: true }).catch(() => []),
1052
- sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
1053
- ]);
1054
- return found ?? [];
1055
- }
1056
- async function answerOf(response) {
1057
- const { status, ok, headers } = response;
1058
- const reader = response.body?.getReader();
1059
- if (!reader) {
1060
- return { status, ok, body: "", truncated: false, headers };
1061
- }
1062
- const read = [];
1063
- let bytes = 0;
1064
- while (bytes <= BODY_LIMIT_BYTES) {
1065
- const { done, value } = await reader.read();
1066
- if (done) {
1067
- return { status, ok, body: text(read), truncated: false, headers };
1068
- }
1069
- read.push(value);
1070
- bytes += value.length;
1071
- }
1072
- await reader.cancel();
1073
- return { status, ok, body: text(read), truncated: true, headers };
1074
- }
1075
- function withoutBody(sent) {
1076
- const headers = { ...sent.headers };
1077
- delete headers["content-type"];
1078
- return { ...sent, method: "GET", headers, body: void 0 };
1079
- }
1080
- function withoutCredentials(sent) {
1081
- const headers = { ...sent.headers };
1082
- for (const named of CREDENTIALS) {
1083
- delete headers[named];
1084
- }
1085
- return { ...sent, headers };
1086
- }
1087
- function text(chunks) {
1088
- const decoder = new TextDecoder();
1089
- let decoded = "";
1090
- for (const chunk of chunks) {
1091
- decoded += decoder.decode(chunk, { stream: true });
1092
- }
1093
- return decoded + decoder.decode();
1094
- }
1095
-
1096
- // ../core/lnurl.ts
1097
- var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1098
- var RESOLVE_TIMEOUT_MS = 3e4;
1099
- var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
1100
- var MIN_PACE_SECS = 1;
1101
- var MAX_PACE_SECS = 3600;
1102
- var MIN_PER_SECOND = 0.01;
1103
- var MAX_PER_SECOND = 100;
1104
- async function resolve(addresses, amountMsat) {
1105
- const served = await firstThatServes(
1106
- addresses,
1107
- (address, deadline) => resolveAddress(address, amountMsat, deadline)
1108
- );
1109
- return served.won;
1110
- }
1111
- async function firstThatServes(addresses, attempt) {
1112
- const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
1113
- const refusals = [];
1114
- for (const address of addresses) {
1115
- if (deadline.aborted) {
1116
- break;
1117
- }
1118
- try {
1119
- return { won: await attempt(address, deadline), refusals };
1120
- } catch (refusal) {
1121
- if (!(refusal instanceof WalletRefused)) {
1122
- throw refusal;
1123
- }
1124
- console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
1125
- refusals.push({ address, reason: refusal.reason });
1126
- }
1127
- }
1128
- throw new NoWalletAvailable(refusals);
1129
- }
1130
- async function probe(address, amountMsat, deadline) {
1131
- const [user, domain] = splitAddress(address);
1132
- if (cannotReleaseAPreimage(domain)) {
1133
- throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
1134
- }
1135
- const pay = await answered(`https://${domain}/.well-known/lnurlp/${user}`, deadline);
1136
- if (pay.tag !== "payRequest" || !pay.callback) {
1137
- throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
1138
- }
1139
- if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
1140
- throw new WalletRefused(
1141
- "amount-not-accepted",
1142
- `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
1143
- );
1144
- }
1145
- return { address, pay };
1146
- }
1147
- async function resolveAddress(address, amountMsat, deadline) {
1148
- const { pay } = await probe(address, amountMsat, deadline);
1149
- const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
1150
- if (!invoice.pr) {
1151
- throw new WalletRefused("unreachable", `${address} returned no invoice`);
1152
- }
1153
- if (!invoice.verify || !publicHttps(invoice.verify)) {
1154
- throw new WalletRefused(
1155
- "cannot-prove-delivery",
1156
- `${address} publishes no usable LUD-21 verify URL`
1157
- );
1158
- }
1159
- const issued = decodeIssued(address, invoice.pr);
1160
- if (issued.amountMsat !== amountMsat) {
1161
- throw new WalletRefused(
1162
- "invoice-refused",
1163
- `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
1164
- );
1165
- }
1166
- if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
1167
- throw new WalletRefused(
1168
- "invoice-refused",
1169
- `${address} does not bind its invoice to the metadata it serves`
1170
- );
1171
- }
1172
- return {
1173
- address,
1174
- bolt11: invoice.pr,
1175
- verifyUrl: invoice.verify,
1176
- paymentHash: issued.paymentHash,
1177
- expiresAt: issued.expiresAt
1178
- };
1179
- }
1180
- async function answered(url, deadline) {
1181
- try {
1182
- return await fetchJson(url, deadline);
1183
- } catch (cause) {
1184
- throw new WalletRefused("unreachable", String(cause));
1185
- }
1186
- }
1187
- function decodeIssued(address, bolt11) {
1188
- const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
1189
- if (paymentHash === null || expiresAt === null) {
1190
- throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
1191
- }
1192
- return { paymentHash, descriptionHash, amountMsat, expiresAt };
1193
- }
1194
- async function checkSettled(verifyUrl, paymentHash) {
1195
- const answer = await answeredJson(verifyUrl);
1196
- const asked = {
1197
- pace: paceAskedFor(answer.headers),
1198
- ceiling: ceilingAskedFor(answer.headers)
1199
- };
1200
- if (!answer.said.settled || !answer.said.preimage) {
1201
- return { preimage: null, ...asked };
1202
- }
1203
- if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
1204
- throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
1205
- }
1206
- return { preimage: answer.said.preimage, ...asked };
1207
- }
1208
- function paceAskedFor(headers) {
1209
- const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
1210
- if (!asked) {
1211
- return null;
1212
- }
1213
- return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
1214
- }
1215
- function ceilingAskedFor(headers) {
1216
- const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
1217
- if (!asked) {
1218
- return null;
1219
- }
1220
- const perWindow = Number(asked[1]);
1221
- const window = asked[2] === void 0 ? 1 : Number(asked[2]);
1222
- if (perWindow === 0 || window === 0) {
1223
- return null;
1224
- }
1225
- return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
1226
- }
1227
- function cannotReleaseAPreimage(host) {
1228
- const lowered = host.toLowerCase();
1229
- return VERIFY_WITHOUT_PREIMAGE.some(
1230
- (known) => lowered === known || lowered.endsWith(`.${known}`)
1231
- );
1232
- }
1233
- function withAmount(callback, amountMsat) {
1234
- const separator = callback.includes("?") ? "&" : "?";
1235
- return `${callback}${separator}amount=${amountMsat}`;
1236
- }
1237
- function splitAddress(address) {
1238
- const [user, domain, ...rest] = address.split("@");
1239
- if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
1240
- throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
1241
- }
1242
- return [user, domain];
1243
- }
1244
- function isNothingButAHost(domain) {
1245
- try {
1246
- const parsed = new URL(`https://${domain}/`);
1247
- return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
1248
- } catch {
1249
- return false;
1250
- }
1251
- }
1252
- async function fetchJson(url, deadline) {
1253
- return (await answeredJson(url, deadline)).said;
1254
- }
1255
- async function answeredJson(url, deadline) {
1256
- const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
1257
- if (!answer.ok) {
1258
- throw new Error(`${url} answered ${answer.status}`);
1259
- }
1260
- if (answer.truncated) {
1261
- throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
1262
- }
1263
- return { said: JSON.parse(answer.body), headers: answer.headers };
1264
- }
1265
-
1266
- // ../core/hmac.ts
1267
- async function hmacHex(secret, payload) {
1268
- const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
1269
- const key = await crypto.subtle.importKey(
1270
- "raw",
1271
- asBuffer(raw),
1272
- { name: "HMAC", hash: "SHA-256" },
1273
- false,
1274
- ["sign"]
1275
- );
1276
- const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
1277
- return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
1278
- }
1279
- function equalInConstantTime(one, other) {
1280
- if (one.length !== other.length) {
1281
- return false;
1282
- }
1283
- let difference = 0;
1284
- for (let at = 0; at < one.length; at++) {
1285
- difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
1286
- }
1287
- return difference === 0;
1288
- }
1289
- function asBuffer(bytes) {
1290
- return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1291
- }
1292
-
1293
- // src/errors.ts
1294
- var ProblemError = class extends Error {
1295
- type;
1296
- title;
1297
- status;
1298
- detail;
1299
- constructor(problem) {
1300
- const title = problem.title ?? "Request failed";
1301
- super(problem.detail ? `${title}: ${problem.detail}` : title);
1302
- this.name = "ProblemError";
1303
- this.type = problem.type ?? "about:blank";
1304
- this.title = title;
1305
- this.status = problem.status ?? 0;
1306
- this.detail = problem.detail ?? null;
1307
- }
1308
- };
1309
- function isProblemType(problem, type) {
1310
- return problem.type === type;
1311
- }
1312
- var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
1313
- var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
1314
- var NoWalletAvailableError = class extends ProblemError {
1315
- wallets;
1316
- constructor(problem, wallets) {
1317
- super({ ...problem, type: NO_WALLET_AVAILABLE });
1318
- this.name = "NoWalletAvailableError";
1319
- this.wallets = wallets;
1320
- }
1321
- };
1322
-
1323
- // src/qr.ts
1324
- var import_uqr = require("uqr");
1325
- function encodeForQr(destination) {
1326
- const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1327
- return `LIGHTNING:${scanned}`;
1328
- }
1329
- function isLnAddress(destination) {
1330
- const at = destination.indexOf("@");
1331
- return at > 0 && destination.slice(at + 1).includes(".");
1332
- }
1333
-
1334
- // src/relay.ts
1335
- var DEFAULT_POLL_EVERY_SECS2 = 5;
1336
- var WALLET = "w";
1337
- function lightningVerifyEndpoint(config) {
1338
- const paced = {
1339
- "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
1340
- };
1341
- return async (request) => {
1342
- const consented = await answerVerifyChallengeRequest(request);
1343
- if (consented !== null) {
1344
- return consented;
1345
- }
1346
- const sealed = new URL(request.url).searchParams.get(WALLET);
1347
- if (sealed === null) {
1348
- return Response.json({ settled: false }, { status: 400 });
1349
- }
1350
- const opened = await unseal(config.secret, sealed);
1351
- if (opened === null) {
1352
- return Response.json({ settled: false }, { status: 403 });
1353
- }
1354
- const wallet = JSON.parse(opened);
1355
- const asked = await checkSettled(wallet.url, wallet.hash).catch(() => null);
1356
- if (asked === null) {
1357
- return Response.json({ settled: false }, { status: 502 });
1358
- }
1359
- return Response.json(
1360
- { settled: asked.preimage !== null, preimage: asked.preimage },
1361
- { headers: paced }
1362
- );
1363
- };
1364
- }
1365
- async function relayedVerifyUrl(endpoint, wallet, secret) {
1366
- const relayed = new URL(endpoint);
1367
- relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
1368
- return relayed.toString();
1369
- }
1370
-
1371
- // src/rail.ts
1372
- var LIGHTNING = "lightning";
1373
- function blindLightningRail(config) {
1374
- return async (order) => {
1375
- const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
1376
- const relay = config.relayVerifyThrough;
1377
- const watched = await config.gateway.watchPayment({
1378
- paymentHash: resolved2.paymentHash,
1379
- verifyUrl: relay ? await relayedVerifyUrl(
1380
- relay.endpoint,
1381
- { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
1382
- relay.secret
1383
- ) : resolved2.verifyUrl,
1384
- expiresAt: resolved2.expiresAt,
1385
- trigger: config.trigger,
1386
- replay: config.replay,
1387
- sealed: await config.sealed?.(order),
1388
- webhookUrl: config.webhookUrl
1389
- });
1390
- return {
1391
- id: watched.id,
1392
- rail: config.name ?? LIGHTNING,
1393
- scan: resolved2.bolt11,
1394
- qr: encodeForQr(resolved2.bolt11),
1395
- expiresAt: resolved2.expiresAt
1396
- };
1397
- };
1398
- }
1399
- function nwcRail(config) {
1191
+ function nwcRail(gateway, config) {
1400
1192
  return async (order) => {
1401
1193
  const invoice = await nwcInvoice(
1402
1194
  config.connection,
1403
- await config.amountMsat(order),
1195
+ await msatForOrder(order, config.amount, config.rate),
1404
1196
  config.description?.(order) ?? order.reference
1405
1197
  );
1406
- const watched = await config.gateway.watchPayment({
1198
+ const watched = await gateway.watch({
1407
1199
  paymentHash: invoice.paymentHash,
1408
1200
  verifyUrl: await nwcVerifyUrl(
1409
1201
  config.verifyThrough.endpoint,
@@ -1413,159 +1205,26 @@ function nwcRail(config) {
1413
1205
  expiresAt: invoice.expiresAt,
1414
1206
  trigger: config.trigger,
1415
1207
  replay: config.replay,
1416
- sealed: await config.sealed?.(order),
1208
+ sealed: config.sealed ? await seal(
1209
+ config.sealed.secret,
1210
+ JSON.stringify(config.sealed.data(order)),
1211
+ invoice.paymentHash
1212
+ ) : void 0,
1417
1213
  webhookUrl: config.webhookUrl
1418
1214
  });
1419
1215
  return {
1420
1216
  id: watched.id,
1421
- rail: config.name ?? LIGHTNING,
1217
+ paymentHash: invoice.paymentHash,
1218
+ rail: config.name ?? "lightning",
1422
1219
  scan: invoice.bolt11,
1423
- qr: encodeForQr(invoice.bolt11),
1220
+ qr: toLightningUri(invoice.bolt11),
1424
1221
  expiresAt: invoice.expiresAt
1425
1222
  };
1426
1223
  };
1427
1224
  }
1428
- async function invoiceFrom(lnAddresses, amountMsat) {
1429
- try {
1430
- return await resolve(lnAddresses, amountMsat);
1431
- } catch (refused) {
1432
- if (refused instanceof NoWalletAvailable) {
1433
- throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
1434
- }
1435
- throw refused;
1436
- }
1437
- }
1438
-
1439
- // src/trigger.ts
1440
- var NONCE_BYTES2 = 16;
1441
- function lnurlPayEndpoint(config) {
1442
- return async (request) => {
1443
- const url = new URL(request.url);
1444
- const asked = url.searchParams.get("to");
1445
- try {
1446
- return asked === null ? await offer(config, url) : await mint(config, url, asked);
1447
- } catch (failure) {
1448
- return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
1449
- }
1450
- };
1451
- }
1452
- function watchTicketEndpoint(config) {
1453
- return async (request) => {
1454
- const offered = await offeredSecret(request);
1455
- if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
1456
- return Response.json({ reason: "not the watch secret" }, { status: 403 });
1457
- }
1458
- return await issue(config);
1459
- };
1460
- }
1461
- function publicWatchTicketEndpoint(config) {
1462
- return () => issue(config);
1463
- }
1464
- async function offer(config, url) {
1465
- const amountMsat = await config.amountMsat();
1466
- const quote = await config.gateway.createQuote({
1467
- lnAddresses: config.lnAddresses,
1468
- amountMsat
1469
- });
1470
- const nonce = randomNonce();
1471
- const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
1472
- callback.searchParams.set("to", quote.lnAddress);
1473
- callback.searchParams.set("msat", String(amountMsat));
1474
- callback.searchParams.set("n", nonce);
1475
- callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
1476
- return Response.json({
1477
- tag: "payRequest",
1478
- callback: callback.toString(),
1479
- metadata: quote.metadata,
1480
- minSendable: amountMsat,
1481
- maxSendable: amountMsat
1482
- });
1483
- }
1484
- async function mint(config, url, address) {
1485
- const amountMsat = Number(url.searchParams.get("msat"));
1486
- const nonce = url.searchParams.get("n") ?? "";
1487
- const signature = url.searchParams.get("sig") ?? "";
1488
- const expected = await sign(config.secret, address, amountMsat, nonce);
1489
- if (!equalInConstantTime(signature, expected)) {
1490
- return refuse("this callback was not signed here");
1491
- }
1492
- const wanted = url.searchParams.get("amount");
1493
- if (wanted !== null && Number(wanted) !== amountMsat) {
1494
- return refuse(`this trigger costs exactly ${amountMsat} msat`);
1495
- }
1496
- const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
1497
- return Response.json({
1498
- status: "OK",
1499
- pr: minted.bolt11,
1500
- routes: [],
1501
- verify: minted.verifyUrl
1502
- });
1503
- }
1504
- async function mintThroughGateway(config, address, amountMsat, nonce) {
1505
- const payment = await config.gateway.createPayment(
1506
- { lnAddresses: [address], amountMsat },
1507
- { idempotencyKey: nonce, trigger: config.watchSecret, replay: config.replay }
1508
- );
1509
- return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
1510
- }
1511
- async function mintBlind(config, address, amountMsat) {
1512
- const resolved2 = await resolve([address], amountMsat);
1513
- const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
1514
- const locked = config.sealed;
1515
- try {
1516
- await config.gateway.watchPayment({
1517
- paymentHash: resolved2.paymentHash,
1518
- verifyUrl: resolved2.verifyUrl,
1519
- expiresAt: resolved2.expiresAt,
1520
- trigger: config.watchSecret,
1521
- replay: config.replay,
1522
- sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
1523
- });
1524
- } catch (refused) {
1525
- if (!alreadyWatched(refused)) {
1526
- throw refused;
1527
- }
1528
- }
1529
- return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
1530
- }
1531
- function alreadyWatched(refused) {
1532
- return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
1533
- }
1534
- function sign(secret, address, amountMsat, nonce) {
1535
- return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
1536
- }
1537
- function randomNonce() {
1538
- const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES2));
1539
- let hex = "";
1540
- for (const byte of bytes) {
1541
- hex += byte.toString(16).padStart(2, "0");
1542
- }
1543
- return hex;
1544
- }
1545
- function refuse(reason) {
1546
- return Response.json({ status: "ERROR", reason });
1547
- }
1548
- async function issue(config) {
1549
- const issued = await config.gateway.createSocketTicket({
1550
- trigger: config.watchSecret,
1551
- replay: config.replay
1552
- });
1553
- return Response.json({
1554
- ticket: issued.ticket,
1555
- expires_at: new Date(issued.expiresAt * 1e3).toISOString()
1556
- });
1557
- }
1558
- async function offeredSecret(request) {
1559
- const body = await request.json().catch(() => null);
1560
- return typeof body?.secret === "string" ? body.secret : "";
1561
- }
1562
1225
  // Annotate the CommonJS export names for ESM import in node:
1563
1226
  0 && (module.exports = {
1564
1227
  askWallet,
1565
- blindLightningRail,
1566
- invoiceFrom,
1567
- lightningVerifyEndpoint,
1568
- lnurlPayEndpoint,
1569
1228
  nwcConnection,
1570
1229
  nwcHoldInvoice,
1571
1230
  nwcInvoice,
@@ -1573,8 +1232,5 @@ async function offeredSecret(request) {
1573
1232
  nwcRail,
1574
1233
  nwcSettlement,
1575
1234
  nwcVerifyEndpoint,
1576
- nwcVerifyUrl,
1577
- publicWatchTicketEndpoint,
1578
- relayedVerifyUrl,
1579
- watchTicketEndpoint
1235
+ nwcVerifyUrl
1580
1236
  });