thunder-bridge 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
31
31
  ThunderBridge: () => ThunderBridge,
32
32
  UnverifiedRecipientError: () => UnverifiedRecipientError,
33
+ bankRail: () => bankRail,
33
34
  bankTransfer: () => bankTransfer,
34
35
  bankVerifyEndpoint: () => bankVerifyEndpoint,
35
36
  bitstamp: () => bitstamp,
@@ -42,7 +43,7 @@ __export(index_exports, {
42
43
  isProblemType: () => isProblemType,
43
44
  isProvablyPaid: () => isProvablyPaid,
44
45
  kraken: () => kraken,
45
- lnurlPayEndpoint: () => lnurlPayEndpoint,
46
+ lightningRail: () => lightningRail,
46
47
  lnurlToDataUrl: () => lnurlToDataUrl,
47
48
  lnurlToSvg: () => lnurlToSvg,
48
49
  medianOf: () => medianOf,
@@ -54,6 +55,8 @@ __export(index_exports, {
54
55
  preimageMatchesHash: () => preimageMatchesHash,
55
56
  proveOrigin: () => proveOrigin,
56
57
  proveSettlement: () => proveSettlement,
58
+ qrToDataUrl: () => qrToDataUrl,
59
+ qrToSvg: () => qrToSvg,
57
60
  seal: () => seal,
58
61
  spdToDataUrl: () => spdToDataUrl,
59
62
  spdToSvg: () => spdToSvg,
@@ -99,10 +102,8 @@ var ProblemError = class extends Error {
99
102
  this.detail = problem.detail ?? null;
100
103
  }
101
104
  };
102
- var NAMESPACE = "urn:problem-type:thunder-bridge:";
103
- var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
104
105
  function isProblemType(problem, type) {
105
- return problem.type === type || problem.type === type.replace(NAMESPACE, NAMESPACE_BEFORE_THE_RENAME);
106
+ return problem.type === type;
106
107
  }
107
108
  var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
108
109
  var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
@@ -413,10 +414,13 @@ function sameOrigin(one, other) {
413
414
  return false;
414
415
  }
415
416
  }
417
+ function publicAddress(literal) {
418
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
419
+ }
416
420
  function hostIsPublic(host) {
417
- if (host.startsWith("[")) return ipv6IsGlobal(host.slice(1, -1));
421
+ if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
418
422
  const name = host.replace(/\.$/, "");
419
- if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return ipv4IsGlobal(name);
423
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
420
424
  const labels = name.split(".");
421
425
  if (labels.length < 2 || labels.some((label) => label === "")) return false;
422
426
  return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
@@ -681,6 +685,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
681
685
  var RECONNECT_DELAY_MS = 3e3;
682
686
  var RECONNECT_CAP_MS = 3e4;
683
687
  var UNANSWERED_ATTEMPTS = 5;
688
+ var TRIGGER_MIN_CHARS = 16;
689
+ var STRANGER_PROBE = "is-this-gateway-yours";
690
+ function unguessable(trigger) {
691
+ if (trigger.length < TRIGGER_MIN_CHARS) {
692
+ throw new Error(
693
+ `a trigger secret is the only thing guarding its stream, and nothing rate limits a guess at it, so it has to be at least ${TRIGGER_MIN_CHARS} characters nobody can predict`
694
+ );
695
+ }
696
+ return trigger;
697
+ }
684
698
  function backoffMs(firstDelay, attempt) {
685
699
  const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
686
700
  return Math.round(grown * (0.5 + Math.random() / 2));
@@ -689,6 +703,7 @@ var ThunderBridge = class {
689
703
  baseUrl;
690
704
  verify;
691
705
  token;
706
+ strangers = null;
692
707
  constructor(baseUrl, options) {
693
708
  this.baseUrl = baseUrl.replace(/\/+$/, "");
694
709
  this.verify = options?.verify ?? true;
@@ -702,6 +717,21 @@ var ThunderBridge = class {
702
717
  get isPrivate() {
703
718
  return this.token !== null;
704
719
  }
720
+ /**
721
+ * Whether the gateway turns away a caller carrying no token, asked by making
722
+ * one unauthenticated read it would have to refuse. `isPrivate` answers only
723
+ * whether you configured a token, which is your side of the arrangement and
724
+ * says nothing about the gateway's, so a made-up token against a public
725
+ * instance reads as private and is not. Asked once and remembered, because an
726
+ * instance does not change its mind. Anything other than a refusal counts as
727
+ * open, so an unreachable gateway fails closed
728
+ */
729
+ async refusesStrangers() {
730
+ this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
731
+ headers: { accept: "application/json" }
732
+ }).then((answer) => answer.status === 401).catch(() => false);
733
+ return await this.strangers;
734
+ }
705
735
  /**
706
736
  * Ask the gateway for an invoice payable to the first address on your list
707
737
  * that can issue a provable one, throws `NoWalletAvailableError` when none can
@@ -713,7 +743,10 @@ var ThunderBridge = class {
713
743
  const response = await fetch(`${this.baseUrl}/incoming-payments`, {
714
744
  method: "POST",
715
745
  headers,
716
- body: createRequestBody(params, options?.trigger ? sha256Hex(options.trigger) : null)
746
+ body: createRequestBody(
747
+ params,
748
+ options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
749
+ )
717
750
  });
718
751
  if (!response.ok) throw await problemFrom(response);
719
752
  const payment = await paymentFrom(response);
@@ -955,7 +988,10 @@ var ThunderBridge = class {
955
988
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
956
989
  method: "POST",
957
990
  headers: this.sending(),
958
- body: watchRequestBody(params, params.trigger ? sha256Hex(params.trigger) : null)
991
+ body: watchRequestBody(
992
+ params,
993
+ params.trigger ? sha256Hex(unguessable(params.trigger)) : null
994
+ )
959
995
  });
960
996
  if (!response.ok) throw await problemFrom(response);
961
997
  const watched = triggerEventFromWire(await response.json().catch(() => null));
@@ -974,7 +1010,7 @@ var ThunderBridge = class {
974
1010
  */
975
1011
  followTrigger(secret, options) {
976
1012
  const base = this.baseUrl.replace(/^http/, "ws");
977
- const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
1013
+ const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
978
1014
  const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
979
1015
  let socket = null;
980
1016
  let retry;
@@ -1181,290 +1217,6 @@ function asBuffer(bytes) {
1181
1217
  return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1182
1218
  }
1183
1219
 
1184
- // ../core/refusal.ts
1185
- var WalletRefused = class extends Error {
1186
- reason;
1187
- constructor(reason, message) {
1188
- super(message);
1189
- this.reason = reason;
1190
- }
1191
- };
1192
- var NoWalletAvailable = class extends Error {
1193
- wallets;
1194
- constructor(wallets) {
1195
- super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
1196
- this.wallets = wallets;
1197
- }
1198
- };
1199
-
1200
- // ../core/lnurl.ts
1201
- var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1202
- var HTTP_TIMEOUT_MS2 = 15e3;
1203
- var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1204
- var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
1205
- var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
1206
- var LNURL_HRP = "lnurl";
1207
- var RESOLVE_TIMEOUT_MS = 3e4;
1208
- async function resolve(addresses, amountMsat) {
1209
- const served = await firstThatServes(
1210
- addresses,
1211
- (address, deadline) => resolveAddress(address, amountMsat, deadline)
1212
- );
1213
- return served.won;
1214
- }
1215
- async function firstThatServes(addresses, attempt) {
1216
- const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
1217
- const refusals3 = [];
1218
- for (const address of addresses) {
1219
- if (deadline.aborted) break;
1220
- try {
1221
- return { won: await attempt(address, deadline), refusals: refusals3 };
1222
- } catch (refusal) {
1223
- if (!(refusal instanceof WalletRefused)) throw refusal;
1224
- console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
1225
- refusals3.push({ address, reason: refusal.reason });
1226
- }
1227
- }
1228
- throw new NoWalletAvailable(refusals3);
1229
- }
1230
- async function probe(address, amountMsat, deadline) {
1231
- const [user, domain] = splitAddress(address);
1232
- if (cannotReleaseAPreimage(domain)) {
1233
- throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
1234
- }
1235
- const pay = await answered(
1236
- `https://${domain}/.well-known/lnurlp/${user}`,
1237
- deadline
1238
- );
1239
- if (pay.tag !== "payRequest" || !pay.callback) {
1240
- throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
1241
- }
1242
- if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
1243
- throw new WalletRefused(
1244
- "amount-not-accepted",
1245
- `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
1246
- );
1247
- }
1248
- return { address, pay };
1249
- }
1250
- async function resolveAddress(address, amountMsat, deadline) {
1251
- const { pay } = await probe(address, amountMsat, deadline);
1252
- const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
1253
- if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
1254
- if (!invoice.verify || !publicHttps(invoice.verify)) {
1255
- throw new WalletRefused(
1256
- "cannot-prove-delivery",
1257
- `${address} publishes no usable LUD-21 verify URL`
1258
- );
1259
- }
1260
- const issued = decodeIssued(address, invoice.pr);
1261
- if (issued.amountMsat !== amountMsat) {
1262
- throw new WalletRefused(
1263
- "invoice-refused",
1264
- `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
1265
- );
1266
- }
1267
- if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
1268
- throw new WalletRefused(
1269
- "invoice-refused",
1270
- `${address} does not bind its invoice to the metadata it serves`
1271
- );
1272
- }
1273
- return {
1274
- address,
1275
- bolt11: invoice.pr,
1276
- verifyUrl: invoice.verify,
1277
- paymentHash: issued.paymentHash,
1278
- expiresAt: issued.expiresAt
1279
- };
1280
- }
1281
- async function answered(url, deadline) {
1282
- try {
1283
- return await fetchJson(url, deadline);
1284
- } catch (cause) {
1285
- throw new WalletRefused("unreachable", String(cause));
1286
- }
1287
- }
1288
- function decodeIssued(address, bolt11) {
1289
- const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
1290
- if (paymentHash === null || expiresAt === null) {
1291
- throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
1292
- }
1293
- return { paymentHash, descriptionHash, amountMsat, expiresAt };
1294
- }
1295
- function cannotReleaseAPreimage(host) {
1296
- const lowered = host.toLowerCase();
1297
- return VERIFY_WITHOUT_PREIMAGE.some(
1298
- (known) => lowered === known || lowered.endsWith(`.${known}`)
1299
- );
1300
- }
1301
- function toLnurl(endpoint) {
1302
- const scheme = parsedScheme(endpoint);
1303
- if (scheme !== "https:" && scheme !== "http:") {
1304
- throw new Error(`${endpoint} is not an http or https URL`);
1305
- }
1306
- const words = toWords(new TextEncoder().encode(endpoint));
1307
- const data = [...words, ...checksumWords(LNURL_HRP, words)];
1308
- return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
1309
- }
1310
- function parsedScheme(endpoint) {
1311
- try {
1312
- return new URL(endpoint).protocol;
1313
- } catch {
1314
- return null;
1315
- }
1316
- }
1317
- function toWords(bytes) {
1318
- const words = [];
1319
- let accumulator = 0;
1320
- let bits = 0;
1321
- for (const byte of bytes) {
1322
- accumulator = accumulator << 8 | byte;
1323
- bits += 8;
1324
- while (bits >= 5) {
1325
- bits -= 5;
1326
- words.push(accumulator >> bits & 31);
1327
- }
1328
- }
1329
- if (bits > 0) words.push(accumulator << 5 - bits & 31);
1330
- return words;
1331
- }
1332
- function checksumWords(hrp, words) {
1333
- const expanded = [...hrp].map((char) => char.charCodeAt(0));
1334
- const values = [
1335
- ...expanded.map((code) => code >> 5),
1336
- 0,
1337
- ...expanded.map((code) => code & 31),
1338
- ...words,
1339
- ...CHECKSUM_SLOTS.map(() => 0)
1340
- ];
1341
- const polymod = bech32Polymod(values) ^ 1;
1342
- return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
1343
- }
1344
- function bech32Polymod(values) {
1345
- let checksum = 1;
1346
- for (const value of values) {
1347
- const top = checksum >> 25;
1348
- checksum = (checksum & 33554431) << 5 ^ value;
1349
- for (let bit = 0; bit < 5; bit++) {
1350
- if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
1351
- }
1352
- }
1353
- return checksum;
1354
- }
1355
- function withAmount(callback, amountMsat) {
1356
- const separator = callback.includes("?") ? "&" : "?";
1357
- return `${callback}${separator}amount=${amountMsat}`;
1358
- }
1359
- function splitAddress(address) {
1360
- const [user, domain, ...rest] = address.split("@");
1361
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
1362
- throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
1363
- }
1364
- return [user, domain];
1365
- }
1366
- async function fetchJson(url, deadline) {
1367
- if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
1368
- const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
1369
- const response = await fetch(url, {
1370
- signal: deadline ? AbortSignal.any([deadline, capped]) : capped,
1371
- headers: { accept: "application/json" }
1372
- });
1373
- if (!response.ok) throw new Error(`${url} answered ${response.status}`);
1374
- return await response.json();
1375
- }
1376
-
1377
- // src/trigger.ts
1378
- var NONCE_BYTES = 16;
1379
- function lnurlPayEndpoint(config) {
1380
- return async (request) => {
1381
- const url = new URL(request.url);
1382
- const asked = url.searchParams.get("to");
1383
- try {
1384
- return asked === null ? await offer(config, url) : await mint(config, url, asked);
1385
- } catch (failure) {
1386
- return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
1387
- }
1388
- };
1389
- }
1390
- async function offer(config, url) {
1391
- const amountMsat = await config.amountMsat();
1392
- const quote = await config.gateway.createQuote({
1393
- lnAddresses: config.lnAddresses,
1394
- amountMsat
1395
- });
1396
- const nonce = randomNonce();
1397
- const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
1398
- callback.searchParams.set("to", quote.lnAddress);
1399
- callback.searchParams.set("msat", String(amountMsat));
1400
- callback.searchParams.set("n", nonce);
1401
- callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
1402
- return Response.json({
1403
- tag: "payRequest",
1404
- callback: callback.toString(),
1405
- metadata: quote.metadata,
1406
- minSendable: amountMsat,
1407
- maxSendable: amountMsat
1408
- });
1409
- }
1410
- async function mint(config, url, address) {
1411
- const amountMsat = Number(url.searchParams.get("msat"));
1412
- const nonce = url.searchParams.get("n") ?? "";
1413
- const signature = url.searchParams.get("sig") ?? "";
1414
- const expected = await sign(config.secret, address, amountMsat, nonce);
1415
- if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
1416
- const wanted = url.searchParams.get("amount");
1417
- if (wanted !== null && Number(wanted) !== amountMsat) {
1418
- return refuse(`this trigger costs exactly ${amountMsat} msat`);
1419
- }
1420
- const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
1421
- return Response.json({
1422
- status: "OK",
1423
- pr: minted.bolt11,
1424
- routes: [],
1425
- verify: minted.verifyUrl
1426
- });
1427
- }
1428
- async function mintThroughGateway(config, address, amountMsat, nonce) {
1429
- const payment = await config.gateway.createPayment(
1430
- { lnAddresses: [address], amountMsat },
1431
- { idempotencyKey: nonce, trigger: config.watchSecret }
1432
- );
1433
- return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
1434
- }
1435
- async function mintBlind(config, address, amountMsat) {
1436
- const resolved = await resolve([address], amountMsat);
1437
- const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
1438
- const locked = config.sealed;
1439
- try {
1440
- await config.gateway.watchPayment({
1441
- paymentHash: resolved.paymentHash,
1442
- verifyUrl: resolved.verifyUrl,
1443
- expiresAt: resolved.expiresAt,
1444
- trigger: config.watchSecret,
1445
- sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
1446
- });
1447
- } catch (refused) {
1448
- if (!alreadyWatched(refused)) throw refused;
1449
- }
1450
- return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
1451
- }
1452
- function alreadyWatched(refused) {
1453
- return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
1454
- }
1455
- function sign(secret, address, amountMsat, nonce) {
1456
- return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
1457
- }
1458
- function randomNonce() {
1459
- const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
1460
- let hex = "";
1461
- for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
1462
- return hex;
1463
- }
1464
- function refuse(reason) {
1465
- return Response.json({ status: "ERROR", reason });
1466
- }
1467
-
1468
1220
  // src/currency.ts
1469
1221
  var ISO_4217_MINOR_UNITS = {
1470
1222
  BHD: 3,
@@ -1515,6 +1267,7 @@ var FORBIDDEN_IN_SPD = "*";
1515
1267
  async function bankTransfer(params) {
1516
1268
  const currency = params.currency ?? DEFAULT_CURRENCY;
1517
1269
  refuseUnusable(params, currency);
1270
+ await refuseAnOpenGateway(params);
1518
1271
  const spd = shortPaymentDescriptor(params, currency);
1519
1272
  const subject = subjectOf(params.reference, params.amountMinor, currency);
1520
1273
  const polling = new URL(params.verifyUrl);
@@ -1581,12 +1334,14 @@ function shortPaymentDescriptor(params, currency) {
1581
1334
  function major(amountMinor, currency) {
1582
1335
  return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
1583
1336
  }
1337
+ async function refuseAnOpenGateway(params) {
1338
+ if (params.allowPublicGateway === true) return;
1339
+ if (await params.gateway.refusesStrangers()) return;
1340
+ throw new Error(
1341
+ "this gateway serves callers with no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Point at one that answers 401 to a stranger, or say allowPublicGateway"
1342
+ );
1343
+ }
1584
1344
  function refuseUnusable(params, currency) {
1585
- if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
1586
- throw new Error(
1587
- "this gateway has no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Give it a token or say allowPublicGateway"
1588
- );
1589
- }
1590
1345
  if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
1591
1346
  if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
1592
1347
  throw new Error("amountMinor must be a whole number of minor units above zero");
@@ -1625,6 +1380,7 @@ var WHERE_A_PAYER_WRITES = [
1625
1380
  USER_IDENTIFICATION,
1626
1381
  PAYER_REFERENCE
1627
1382
  ];
1383
+ var UTC_OFFSET = /^[+-]\d{4}$/;
1628
1384
  function fioStatement(config) {
1629
1385
  const named = typeof config.token === "string" ? [config.token] : config.token;
1630
1386
  if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
@@ -1677,9 +1433,21 @@ function asCredit(transaction) {
1677
1433
  amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
1678
1434
  currency: textIn(transaction[CURRENCY]),
1679
1435
  reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
1680
- bookedAt: Math.floor(numberIn(transaction[BOOKED_AT]) / MILLIS)
1436
+ bookedAt: bookedAtIn(transaction[BOOKED_AT])
1681
1437
  };
1682
1438
  }
1439
+ function bookedAtIn(cell) {
1440
+ const value = cell?.value;
1441
+ if (typeof value === "number") return Math.floor(value / MILLIS);
1442
+ const at = Date.parse(asIso8601(textIn(cell)));
1443
+ return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
1444
+ }
1445
+ function asIso8601(booked) {
1446
+ const day = booked.slice(0, DATE_CHARS);
1447
+ const zone = booked.slice(DATE_CHARS);
1448
+ if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
1449
+ return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
1450
+ }
1683
1451
  function numberIn(cell) {
1684
1452
  const value = Number(cell?.value ?? Number.NaN);
1685
1453
  return Number.isFinite(value) ? value : 0;
@@ -1692,6 +1460,180 @@ function asDate(unix) {
1692
1460
  return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
1693
1461
  }
1694
1462
 
1463
+ // ../core/lnurl.ts
1464
+ var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1465
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
1466
+ var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
1467
+ var LNURL_HRP = "lnurl";
1468
+ function toLnurl(endpoint) {
1469
+ const scheme = parsedScheme(endpoint);
1470
+ if (scheme !== "https:" && scheme !== "http:") {
1471
+ throw new Error(`${endpoint} is not an http or https URL`);
1472
+ }
1473
+ const words = toWords(new TextEncoder().encode(endpoint));
1474
+ const data = [...words, ...checksumWords(LNURL_HRP, words)];
1475
+ return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
1476
+ }
1477
+ function parsedScheme(endpoint) {
1478
+ try {
1479
+ return new URL(endpoint).protocol;
1480
+ } catch {
1481
+ return null;
1482
+ }
1483
+ }
1484
+ function toWords(bytes) {
1485
+ const words = [];
1486
+ let accumulator = 0;
1487
+ let bits = 0;
1488
+ for (const byte of bytes) {
1489
+ accumulator = accumulator << 8 | byte;
1490
+ bits += 8;
1491
+ while (bits >= 5) {
1492
+ bits -= 5;
1493
+ words.push(accumulator >> bits & 31);
1494
+ }
1495
+ }
1496
+ if (bits > 0) words.push(accumulator << 5 - bits & 31);
1497
+ return words;
1498
+ }
1499
+ function checksumWords(hrp, words) {
1500
+ const expanded = [...hrp].map((char) => char.charCodeAt(0));
1501
+ const values = [
1502
+ ...expanded.map((code) => code >> 5),
1503
+ 0,
1504
+ ...expanded.map((code) => code & 31),
1505
+ ...words,
1506
+ ...CHECKSUM_SLOTS.map(() => 0)
1507
+ ];
1508
+ const polymod = bech32Polymod(values) ^ 1;
1509
+ return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
1510
+ }
1511
+ function bech32Polymod(values) {
1512
+ let checksum = 1;
1513
+ for (const value of values) {
1514
+ const top = checksum >> 25;
1515
+ checksum = (checksum & 33554431) << 5 ^ value;
1516
+ for (let bit = 0; bit < 5; bit++) {
1517
+ if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
1518
+ }
1519
+ }
1520
+ return checksum;
1521
+ }
1522
+
1523
+ // src/qr.ts
1524
+ var import_uqr = require("uqr");
1525
+ var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1526
+ function encodeForQr(destination) {
1527
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1528
+ return `LIGHTNING:${scanned}`;
1529
+ }
1530
+ function invoiceToSvg(destination, options) {
1531
+ return svgOf(encodeForQr(destination), options);
1532
+ }
1533
+ function invoiceToDataUrl(destination, options) {
1534
+ return asDataUrl(invoiceToSvg(destination, options));
1535
+ }
1536
+ function lnurlToSvg(endpoint, options) {
1537
+ return svgOf(toLnurl(endpoint), options);
1538
+ }
1539
+ function lnurlToDataUrl(endpoint, options) {
1540
+ return asDataUrl(lnurlToSvg(endpoint, options));
1541
+ }
1542
+ function spdToSvg(spd, options) {
1543
+ return svgOf(spd, options);
1544
+ }
1545
+ function spdToDataUrl(spd, options) {
1546
+ return asDataUrl(spdToSvg(spd, options));
1547
+ }
1548
+ function qrToSvg(payload, options) {
1549
+ return svgOf(payload, options);
1550
+ }
1551
+ function qrToDataUrl(payload, options) {
1552
+ return asDataUrl(qrToSvg(payload, options));
1553
+ }
1554
+ function svgOf(scanned, options) {
1555
+ const size = options?.size ?? 256;
1556
+ const color = options?.color ?? "#000";
1557
+ if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1558
+ return renderSvg(scanned, size, color);
1559
+ }
1560
+ function asDataUrl(svg) {
1561
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1562
+ }
1563
+ function isLnAddress(destination) {
1564
+ const at = destination.indexOf("@");
1565
+ return at > 0 && destination.slice(at + 1).includes(".");
1566
+ }
1567
+ function renderSvg(content, size, color) {
1568
+ const { data } = (0, import_uqr.encode)(content);
1569
+ const modules = data.length;
1570
+ const cellSize = size / modules;
1571
+ let path = "";
1572
+ for (let y = 0; y < modules; y++) {
1573
+ for (let x = 0; x < modules; x++) {
1574
+ if (data[y][x]) {
1575
+ path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1576
+ }
1577
+ }
1578
+ }
1579
+ return [
1580
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1581
+ `<rect width="100%" height="100%" fill="white"/>`,
1582
+ `<path d="${path}" fill="${color}"/>`,
1583
+ `</svg>`
1584
+ ].join("");
1585
+ }
1586
+
1587
+ // src/rail.ts
1588
+ var BANK = "bank";
1589
+ var LIGHTNING = "lightning";
1590
+ function bankRail(config) {
1591
+ return async (order) => {
1592
+ const expiresAt = config.expiresAt(order);
1593
+ const transfer = await bankTransfer({
1594
+ gateway: config.gateway,
1595
+ secret: config.secret,
1596
+ iban: config.iban,
1597
+ verifyUrl: config.verifyUrl,
1598
+ reference: order.reference,
1599
+ amountMinor: order.amountMinor,
1600
+ currency: order.currency,
1601
+ expiresAt,
1602
+ trigger: config.trigger,
1603
+ sealed: await config.sealed?.(order),
1604
+ variableSymbol: config.variableSymbol?.(order),
1605
+ allowPublicGateway: config.allowPublicGateway
1606
+ });
1607
+ return {
1608
+ id: transfer.id,
1609
+ rail: config.name ?? BANK,
1610
+ scan: transfer.spd,
1611
+ qr: transfer.spd,
1612
+ expiresAt
1613
+ };
1614
+ };
1615
+ }
1616
+ function lightningRail(config) {
1617
+ return async (order) => {
1618
+ const payment = await config.gateway.createPayment(
1619
+ {
1620
+ lnAddresses: config.lnAddresses,
1621
+ amountMsat: await config.amountMsat(order),
1622
+ webhookUrl: config.webhookUrl,
1623
+ webhookSecret: config.webhookSecret
1624
+ },
1625
+ { idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
1626
+ );
1627
+ return {
1628
+ id: payment.id,
1629
+ rail: config.name ?? LIGHTNING,
1630
+ scan: payment.bolt11,
1631
+ qr: encodeForQr(payment.bolt11),
1632
+ expiresAt: payment.expiresAt
1633
+ };
1634
+ };
1635
+ }
1636
+
1695
1637
  // src/price.ts
1696
1638
  var MSAT_PER_BTC2 = 1e11;
1697
1639
  var BASIS_POINTS = 1e4;
@@ -1814,64 +1756,6 @@ function refusals2(answers) {
1814
1756
  ).join("; ");
1815
1757
  }
1816
1758
 
1817
- // src/qr.ts
1818
- var import_uqr = require("uqr");
1819
- var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1820
- function encodeForQr(destination) {
1821
- const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1822
- return `LIGHTNING:${scanned}`;
1823
- }
1824
- function invoiceToSvg(destination, options) {
1825
- return svgOf(encodeForQr(destination), options);
1826
- }
1827
- function invoiceToDataUrl(destination, options) {
1828
- return asDataUrl(invoiceToSvg(destination, options));
1829
- }
1830
- function lnurlToSvg(endpoint, options) {
1831
- return svgOf(toLnurl(endpoint), options);
1832
- }
1833
- function lnurlToDataUrl(endpoint, options) {
1834
- return asDataUrl(lnurlToSvg(endpoint, options));
1835
- }
1836
- function spdToSvg(spd, options) {
1837
- return svgOf(spd, options);
1838
- }
1839
- function spdToDataUrl(spd, options) {
1840
- return asDataUrl(spdToSvg(spd, options));
1841
- }
1842
- function svgOf(scanned, options) {
1843
- const size = options?.size ?? 256;
1844
- const color = options?.color ?? "#000";
1845
- if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1846
- return renderSvg(scanned, size, color);
1847
- }
1848
- function asDataUrl(svg) {
1849
- return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1850
- }
1851
- function isLnAddress(destination) {
1852
- const at = destination.indexOf("@");
1853
- return at > 0 && destination.slice(at + 1).includes(".");
1854
- }
1855
- function renderSvg(content, size, color) {
1856
- const { data } = (0, import_uqr.encode)(content);
1857
- const modules = data.length;
1858
- const cellSize = size / modules;
1859
- let path = "";
1860
- for (let y = 0; y < modules; y++) {
1861
- for (let x = 0; x < modules; x++) {
1862
- if (data[y][x]) {
1863
- path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1864
- }
1865
- }
1866
- }
1867
- return [
1868
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1869
- `<rect width="100%" height="100%" fill="white"/>`,
1870
- `<path d="${path}" fill="${color}"/>`,
1871
- `</svg>`
1872
- ].join("");
1873
- }
1874
-
1875
1759
  // src/webhook.ts
1876
1760
  var SIGNATURE_HEADER = "x-signature";
1877
1761
  var TIMESTAMP_HEADER = "x-timestamp";
@@ -1880,7 +1764,7 @@ var DEFAULT_TOLERANCE_SECS = 300;
1880
1764
  async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
1881
1765
  if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1882
1766
  const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
1883
- return equalInConstantTime(await sign2(secret, timestamp, body), received.toLowerCase());
1767
+ return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
1884
1768
  }
1885
1769
  async function parseWebhook(body, signature, secret, timestamp, options = {}) {
1886
1770
  if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
@@ -1902,7 +1786,7 @@ function recent(timestamp, toleranceSecs) {
1902
1786
  if (!Number.isFinite(sent)) return false;
1903
1787
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1904
1788
  }
1905
- async function sign2(secret, timestamp, body) {
1789
+ async function sign(secret, timestamp, body) {
1906
1790
  return hmacHex(secret, signed(timestamp, body));
1907
1791
  }
1908
1792
  function signed(timestamp, body) {
@@ -1926,6 +1810,7 @@ function signed(timestamp, body) {
1926
1810
  REQUEST_IN_FLIGHT,
1927
1811
  ThunderBridge,
1928
1812
  UnverifiedRecipientError,
1813
+ bankRail,
1929
1814
  bankTransfer,
1930
1815
  bankVerifyEndpoint,
1931
1816
  bitstamp,
@@ -1938,7 +1823,7 @@ function signed(timestamp, body) {
1938
1823
  isProblemType,
1939
1824
  isProvablyPaid,
1940
1825
  kraken,
1941
- lnurlPayEndpoint,
1826
+ lightningRail,
1942
1827
  lnurlToDataUrl,
1943
1828
  lnurlToSvg,
1944
1829
  medianOf,
@@ -1950,6 +1835,8 @@ function signed(timestamp, body) {
1950
1835
  preimageMatchesHash,
1951
1836
  proveOrigin,
1952
1837
  proveSettlement,
1838
+ qrToDataUrl,
1839
+ qrToSvg,
1953
1840
  seal,
1954
1841
  spdToDataUrl,
1955
1842
  spdToSvg,