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.js CHANGED
@@ -34,10 +34,8 @@ var ProblemError = class extends Error {
34
34
  this.detail = problem.detail ?? null;
35
35
  }
36
36
  };
37
- var NAMESPACE = "urn:problem-type:thunder-bridge:";
38
- var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
39
37
  function isProblemType(problem, type) {
40
- return problem.type === type || problem.type === type.replace(NAMESPACE, NAMESPACE_BEFORE_THE_RENAME);
38
+ return problem.type === type;
41
39
  }
42
40
  var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
43
41
  var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
@@ -348,10 +346,13 @@ function sameOrigin(one, other) {
348
346
  return false;
349
347
  }
350
348
  }
349
+ function publicAddress(literal) {
350
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
351
+ }
351
352
  function hostIsPublic(host) {
352
- if (host.startsWith("[")) return ipv6IsGlobal(host.slice(1, -1));
353
+ if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
353
354
  const name = host.replace(/\.$/, "");
354
- if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return ipv4IsGlobal(name);
355
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
355
356
  const labels = name.split(".");
356
357
  if (labels.length < 2 || labels.some((label) => label === "")) return false;
357
358
  return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
@@ -616,6 +617,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
616
617
  var RECONNECT_DELAY_MS = 3e3;
617
618
  var RECONNECT_CAP_MS = 3e4;
618
619
  var UNANSWERED_ATTEMPTS = 5;
620
+ var TRIGGER_MIN_CHARS = 16;
621
+ var STRANGER_PROBE = "is-this-gateway-yours";
622
+ function unguessable(trigger) {
623
+ if (trigger.length < TRIGGER_MIN_CHARS) {
624
+ throw new Error(
625
+ `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`
626
+ );
627
+ }
628
+ return trigger;
629
+ }
619
630
  function backoffMs(firstDelay, attempt) {
620
631
  const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
621
632
  return Math.round(grown * (0.5 + Math.random() / 2));
@@ -624,6 +635,7 @@ var ThunderBridge = class {
624
635
  baseUrl;
625
636
  verify;
626
637
  token;
638
+ strangers = null;
627
639
  constructor(baseUrl, options) {
628
640
  this.baseUrl = baseUrl.replace(/\/+$/, "");
629
641
  this.verify = options?.verify ?? true;
@@ -637,6 +649,21 @@ var ThunderBridge = class {
637
649
  get isPrivate() {
638
650
  return this.token !== null;
639
651
  }
652
+ /**
653
+ * Whether the gateway turns away a caller carrying no token, asked by making
654
+ * one unauthenticated read it would have to refuse. `isPrivate` answers only
655
+ * whether you configured a token, which is your side of the arrangement and
656
+ * says nothing about the gateway's, so a made-up token against a public
657
+ * instance reads as private and is not. Asked once and remembered, because an
658
+ * instance does not change its mind. Anything other than a refusal counts as
659
+ * open, so an unreachable gateway fails closed
660
+ */
661
+ async refusesStrangers() {
662
+ this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
663
+ headers: { accept: "application/json" }
664
+ }).then((answer) => answer.status === 401).catch(() => false);
665
+ return await this.strangers;
666
+ }
640
667
  /**
641
668
  * Ask the gateway for an invoice payable to the first address on your list
642
669
  * that can issue a provable one, throws `NoWalletAvailableError` when none can
@@ -648,7 +675,10 @@ var ThunderBridge = class {
648
675
  const response = await fetch(`${this.baseUrl}/incoming-payments`, {
649
676
  method: "POST",
650
677
  headers,
651
- body: createRequestBody(params, options?.trigger ? sha256Hex(options.trigger) : null)
678
+ body: createRequestBody(
679
+ params,
680
+ options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
681
+ )
652
682
  });
653
683
  if (!response.ok) throw await problemFrom(response);
654
684
  const payment = await paymentFrom(response);
@@ -890,7 +920,10 @@ var ThunderBridge = class {
890
920
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
891
921
  method: "POST",
892
922
  headers: this.sending(),
893
- body: watchRequestBody(params, params.trigger ? sha256Hex(params.trigger) : null)
923
+ body: watchRequestBody(
924
+ params,
925
+ params.trigger ? sha256Hex(unguessable(params.trigger)) : null
926
+ )
894
927
  });
895
928
  if (!response.ok) throw await problemFrom(response);
896
929
  const watched = triggerEventFromWire(await response.json().catch(() => null));
@@ -909,7 +942,7 @@ var ThunderBridge = class {
909
942
  */
910
943
  followTrigger(secret, options) {
911
944
  const base = this.baseUrl.replace(/^http/, "ws");
912
- const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
945
+ const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
913
946
  const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
914
947
  let socket = null;
915
948
  let retry;
@@ -1116,290 +1149,6 @@ function asBuffer(bytes) {
1116
1149
  return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1117
1150
  }
1118
1151
 
1119
- // ../core/refusal.ts
1120
- var WalletRefused = class extends Error {
1121
- reason;
1122
- constructor(reason, message) {
1123
- super(message);
1124
- this.reason = reason;
1125
- }
1126
- };
1127
- var NoWalletAvailable = class extends Error {
1128
- wallets;
1129
- constructor(wallets) {
1130
- super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
1131
- this.wallets = wallets;
1132
- }
1133
- };
1134
-
1135
- // ../core/lnurl.ts
1136
- var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1137
- var HTTP_TIMEOUT_MS2 = 15e3;
1138
- var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1139
- var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
1140
- var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
1141
- var LNURL_HRP = "lnurl";
1142
- var RESOLVE_TIMEOUT_MS = 3e4;
1143
- async function resolve(addresses, amountMsat) {
1144
- const served = await firstThatServes(
1145
- addresses,
1146
- (address, deadline) => resolveAddress(address, amountMsat, deadline)
1147
- );
1148
- return served.won;
1149
- }
1150
- async function firstThatServes(addresses, attempt) {
1151
- const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
1152
- const refusals3 = [];
1153
- for (const address of addresses) {
1154
- if (deadline.aborted) break;
1155
- try {
1156
- return { won: await attempt(address, deadline), refusals: refusals3 };
1157
- } catch (refusal) {
1158
- if (!(refusal instanceof WalletRefused)) throw refusal;
1159
- console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
1160
- refusals3.push({ address, reason: refusal.reason });
1161
- }
1162
- }
1163
- throw new NoWalletAvailable(refusals3);
1164
- }
1165
- async function probe(address, amountMsat, deadline) {
1166
- const [user, domain] = splitAddress(address);
1167
- if (cannotReleaseAPreimage(domain)) {
1168
- throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
1169
- }
1170
- const pay = await answered(
1171
- `https://${domain}/.well-known/lnurlp/${user}`,
1172
- deadline
1173
- );
1174
- if (pay.tag !== "payRequest" || !pay.callback) {
1175
- throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
1176
- }
1177
- if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
1178
- throw new WalletRefused(
1179
- "amount-not-accepted",
1180
- `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
1181
- );
1182
- }
1183
- return { address, pay };
1184
- }
1185
- async function resolveAddress(address, amountMsat, deadline) {
1186
- const { pay } = await probe(address, amountMsat, deadline);
1187
- const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
1188
- if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
1189
- if (!invoice.verify || !publicHttps(invoice.verify)) {
1190
- throw new WalletRefused(
1191
- "cannot-prove-delivery",
1192
- `${address} publishes no usable LUD-21 verify URL`
1193
- );
1194
- }
1195
- const issued = decodeIssued(address, invoice.pr);
1196
- if (issued.amountMsat !== amountMsat) {
1197
- throw new WalletRefused(
1198
- "invoice-refused",
1199
- `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
1200
- );
1201
- }
1202
- if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
1203
- throw new WalletRefused(
1204
- "invoice-refused",
1205
- `${address} does not bind its invoice to the metadata it serves`
1206
- );
1207
- }
1208
- return {
1209
- address,
1210
- bolt11: invoice.pr,
1211
- verifyUrl: invoice.verify,
1212
- paymentHash: issued.paymentHash,
1213
- expiresAt: issued.expiresAt
1214
- };
1215
- }
1216
- async function answered(url, deadline) {
1217
- try {
1218
- return await fetchJson(url, deadline);
1219
- } catch (cause) {
1220
- throw new WalletRefused("unreachable", String(cause));
1221
- }
1222
- }
1223
- function decodeIssued(address, bolt11) {
1224
- const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
1225
- if (paymentHash === null || expiresAt === null) {
1226
- throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
1227
- }
1228
- return { paymentHash, descriptionHash, amountMsat, expiresAt };
1229
- }
1230
- function cannotReleaseAPreimage(host) {
1231
- const lowered = host.toLowerCase();
1232
- return VERIFY_WITHOUT_PREIMAGE.some(
1233
- (known) => lowered === known || lowered.endsWith(`.${known}`)
1234
- );
1235
- }
1236
- function toLnurl(endpoint) {
1237
- const scheme = parsedScheme(endpoint);
1238
- if (scheme !== "https:" && scheme !== "http:") {
1239
- throw new Error(`${endpoint} is not an http or https URL`);
1240
- }
1241
- const words = toWords(new TextEncoder().encode(endpoint));
1242
- const data = [...words, ...checksumWords(LNURL_HRP, words)];
1243
- return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
1244
- }
1245
- function parsedScheme(endpoint) {
1246
- try {
1247
- return new URL(endpoint).protocol;
1248
- } catch {
1249
- return null;
1250
- }
1251
- }
1252
- function toWords(bytes) {
1253
- const words = [];
1254
- let accumulator = 0;
1255
- let bits = 0;
1256
- for (const byte of bytes) {
1257
- accumulator = accumulator << 8 | byte;
1258
- bits += 8;
1259
- while (bits >= 5) {
1260
- bits -= 5;
1261
- words.push(accumulator >> bits & 31);
1262
- }
1263
- }
1264
- if (bits > 0) words.push(accumulator << 5 - bits & 31);
1265
- return words;
1266
- }
1267
- function checksumWords(hrp, words) {
1268
- const expanded = [...hrp].map((char) => char.charCodeAt(0));
1269
- const values = [
1270
- ...expanded.map((code) => code >> 5),
1271
- 0,
1272
- ...expanded.map((code) => code & 31),
1273
- ...words,
1274
- ...CHECKSUM_SLOTS.map(() => 0)
1275
- ];
1276
- const polymod = bech32Polymod(values) ^ 1;
1277
- return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
1278
- }
1279
- function bech32Polymod(values) {
1280
- let checksum = 1;
1281
- for (const value of values) {
1282
- const top = checksum >> 25;
1283
- checksum = (checksum & 33554431) << 5 ^ value;
1284
- for (let bit = 0; bit < 5; bit++) {
1285
- if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
1286
- }
1287
- }
1288
- return checksum;
1289
- }
1290
- function withAmount(callback, amountMsat) {
1291
- const separator = callback.includes("?") ? "&" : "?";
1292
- return `${callback}${separator}amount=${amountMsat}`;
1293
- }
1294
- function splitAddress(address) {
1295
- const [user, domain, ...rest] = address.split("@");
1296
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
1297
- throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
1298
- }
1299
- return [user, domain];
1300
- }
1301
- async function fetchJson(url, deadline) {
1302
- if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
1303
- const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
1304
- const response = await fetch(url, {
1305
- signal: deadline ? AbortSignal.any([deadline, capped]) : capped,
1306
- headers: { accept: "application/json" }
1307
- });
1308
- if (!response.ok) throw new Error(`${url} answered ${response.status}`);
1309
- return await response.json();
1310
- }
1311
-
1312
- // src/trigger.ts
1313
- var NONCE_BYTES = 16;
1314
- function lnurlPayEndpoint(config) {
1315
- return async (request) => {
1316
- const url = new URL(request.url);
1317
- const asked = url.searchParams.get("to");
1318
- try {
1319
- return asked === null ? await offer(config, url) : await mint(config, url, asked);
1320
- } catch (failure) {
1321
- return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
1322
- }
1323
- };
1324
- }
1325
- async function offer(config, url) {
1326
- const amountMsat = await config.amountMsat();
1327
- const quote = await config.gateway.createQuote({
1328
- lnAddresses: config.lnAddresses,
1329
- amountMsat
1330
- });
1331
- const nonce = randomNonce();
1332
- const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
1333
- callback.searchParams.set("to", quote.lnAddress);
1334
- callback.searchParams.set("msat", String(amountMsat));
1335
- callback.searchParams.set("n", nonce);
1336
- callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
1337
- return Response.json({
1338
- tag: "payRequest",
1339
- callback: callback.toString(),
1340
- metadata: quote.metadata,
1341
- minSendable: amountMsat,
1342
- maxSendable: amountMsat
1343
- });
1344
- }
1345
- async function mint(config, url, address) {
1346
- const amountMsat = Number(url.searchParams.get("msat"));
1347
- const nonce = url.searchParams.get("n") ?? "";
1348
- const signature = url.searchParams.get("sig") ?? "";
1349
- const expected = await sign(config.secret, address, amountMsat, nonce);
1350
- if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
1351
- const wanted = url.searchParams.get("amount");
1352
- if (wanted !== null && Number(wanted) !== amountMsat) {
1353
- return refuse(`this trigger costs exactly ${amountMsat} msat`);
1354
- }
1355
- const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
1356
- return Response.json({
1357
- status: "OK",
1358
- pr: minted.bolt11,
1359
- routes: [],
1360
- verify: minted.verifyUrl
1361
- });
1362
- }
1363
- async function mintThroughGateway(config, address, amountMsat, nonce) {
1364
- const payment = await config.gateway.createPayment(
1365
- { lnAddresses: [address], amountMsat },
1366
- { idempotencyKey: nonce, trigger: config.watchSecret }
1367
- );
1368
- return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
1369
- }
1370
- async function mintBlind(config, address, amountMsat) {
1371
- const resolved = await resolve([address], amountMsat);
1372
- const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
1373
- const locked = config.sealed;
1374
- try {
1375
- await config.gateway.watchPayment({
1376
- paymentHash: resolved.paymentHash,
1377
- verifyUrl: resolved.verifyUrl,
1378
- expiresAt: resolved.expiresAt,
1379
- trigger: config.watchSecret,
1380
- sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
1381
- });
1382
- } catch (refused) {
1383
- if (!alreadyWatched(refused)) throw refused;
1384
- }
1385
- return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
1386
- }
1387
- function alreadyWatched(refused) {
1388
- return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
1389
- }
1390
- function sign(secret, address, amountMsat, nonce) {
1391
- return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
1392
- }
1393
- function randomNonce() {
1394
- const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
1395
- let hex = "";
1396
- for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
1397
- return hex;
1398
- }
1399
- function refuse(reason) {
1400
- return Response.json({ status: "ERROR", reason });
1401
- }
1402
-
1403
1152
  // src/currency.ts
1404
1153
  var ISO_4217_MINOR_UNITS = {
1405
1154
  BHD: 3,
@@ -1450,6 +1199,7 @@ var FORBIDDEN_IN_SPD = "*";
1450
1199
  async function bankTransfer(params) {
1451
1200
  const currency = params.currency ?? DEFAULT_CURRENCY;
1452
1201
  refuseUnusable(params, currency);
1202
+ await refuseAnOpenGateway(params);
1453
1203
  const spd = shortPaymentDescriptor(params, currency);
1454
1204
  const subject = subjectOf(params.reference, params.amountMinor, currency);
1455
1205
  const polling = new URL(params.verifyUrl);
@@ -1516,12 +1266,14 @@ function shortPaymentDescriptor(params, currency) {
1516
1266
  function major(amountMinor, currency) {
1517
1267
  return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
1518
1268
  }
1269
+ async function refuseAnOpenGateway(params) {
1270
+ if (params.allowPublicGateway === true) return;
1271
+ if (await params.gateway.refusesStrangers()) return;
1272
+ throw new Error(
1273
+ "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"
1274
+ );
1275
+ }
1519
1276
  function refuseUnusable(params, currency) {
1520
- if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
1521
- throw new Error(
1522
- "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"
1523
- );
1524
- }
1525
1277
  if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
1526
1278
  if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
1527
1279
  throw new Error("amountMinor must be a whole number of minor units above zero");
@@ -1560,6 +1312,7 @@ var WHERE_A_PAYER_WRITES = [
1560
1312
  USER_IDENTIFICATION,
1561
1313
  PAYER_REFERENCE
1562
1314
  ];
1315
+ var UTC_OFFSET = /^[+-]\d{4}$/;
1563
1316
  function fioStatement(config) {
1564
1317
  const named = typeof config.token === "string" ? [config.token] : config.token;
1565
1318
  if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
@@ -1612,9 +1365,21 @@ function asCredit(transaction) {
1612
1365
  amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
1613
1366
  currency: textIn(transaction[CURRENCY]),
1614
1367
  reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
1615
- bookedAt: Math.floor(numberIn(transaction[BOOKED_AT]) / MILLIS)
1368
+ bookedAt: bookedAtIn(transaction[BOOKED_AT])
1616
1369
  };
1617
1370
  }
1371
+ function bookedAtIn(cell) {
1372
+ const value = cell?.value;
1373
+ if (typeof value === "number") return Math.floor(value / MILLIS);
1374
+ const at = Date.parse(asIso8601(textIn(cell)));
1375
+ return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
1376
+ }
1377
+ function asIso8601(booked) {
1378
+ const day = booked.slice(0, DATE_CHARS);
1379
+ const zone = booked.slice(DATE_CHARS);
1380
+ if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
1381
+ return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
1382
+ }
1618
1383
  function numberIn(cell) {
1619
1384
  const value = Number(cell?.value ?? Number.NaN);
1620
1385
  return Number.isFinite(value) ? value : 0;
@@ -1627,6 +1392,180 @@ function asDate(unix) {
1627
1392
  return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
1628
1393
  }
1629
1394
 
1395
+ // ../core/lnurl.ts
1396
+ var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1397
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
1398
+ var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
1399
+ var LNURL_HRP = "lnurl";
1400
+ function toLnurl(endpoint) {
1401
+ const scheme = parsedScheme(endpoint);
1402
+ if (scheme !== "https:" && scheme !== "http:") {
1403
+ throw new Error(`${endpoint} is not an http or https URL`);
1404
+ }
1405
+ const words = toWords(new TextEncoder().encode(endpoint));
1406
+ const data = [...words, ...checksumWords(LNURL_HRP, words)];
1407
+ return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
1408
+ }
1409
+ function parsedScheme(endpoint) {
1410
+ try {
1411
+ return new URL(endpoint).protocol;
1412
+ } catch {
1413
+ return null;
1414
+ }
1415
+ }
1416
+ function toWords(bytes) {
1417
+ const words = [];
1418
+ let accumulator = 0;
1419
+ let bits = 0;
1420
+ for (const byte of bytes) {
1421
+ accumulator = accumulator << 8 | byte;
1422
+ bits += 8;
1423
+ while (bits >= 5) {
1424
+ bits -= 5;
1425
+ words.push(accumulator >> bits & 31);
1426
+ }
1427
+ }
1428
+ if (bits > 0) words.push(accumulator << 5 - bits & 31);
1429
+ return words;
1430
+ }
1431
+ function checksumWords(hrp, words) {
1432
+ const expanded = [...hrp].map((char) => char.charCodeAt(0));
1433
+ const values = [
1434
+ ...expanded.map((code) => code >> 5),
1435
+ 0,
1436
+ ...expanded.map((code) => code & 31),
1437
+ ...words,
1438
+ ...CHECKSUM_SLOTS.map(() => 0)
1439
+ ];
1440
+ const polymod = bech32Polymod(values) ^ 1;
1441
+ return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
1442
+ }
1443
+ function bech32Polymod(values) {
1444
+ let checksum = 1;
1445
+ for (const value of values) {
1446
+ const top = checksum >> 25;
1447
+ checksum = (checksum & 33554431) << 5 ^ value;
1448
+ for (let bit = 0; bit < 5; bit++) {
1449
+ if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
1450
+ }
1451
+ }
1452
+ return checksum;
1453
+ }
1454
+
1455
+ // src/qr.ts
1456
+ import { encode } from "uqr";
1457
+ var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1458
+ function encodeForQr(destination) {
1459
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1460
+ return `LIGHTNING:${scanned}`;
1461
+ }
1462
+ function invoiceToSvg(destination, options) {
1463
+ return svgOf(encodeForQr(destination), options);
1464
+ }
1465
+ function invoiceToDataUrl(destination, options) {
1466
+ return asDataUrl(invoiceToSvg(destination, options));
1467
+ }
1468
+ function lnurlToSvg(endpoint, options) {
1469
+ return svgOf(toLnurl(endpoint), options);
1470
+ }
1471
+ function lnurlToDataUrl(endpoint, options) {
1472
+ return asDataUrl(lnurlToSvg(endpoint, options));
1473
+ }
1474
+ function spdToSvg(spd, options) {
1475
+ return svgOf(spd, options);
1476
+ }
1477
+ function spdToDataUrl(spd, options) {
1478
+ return asDataUrl(spdToSvg(spd, options));
1479
+ }
1480
+ function qrToSvg(payload, options) {
1481
+ return svgOf(payload, options);
1482
+ }
1483
+ function qrToDataUrl(payload, options) {
1484
+ return asDataUrl(qrToSvg(payload, options));
1485
+ }
1486
+ function svgOf(scanned, options) {
1487
+ const size = options?.size ?? 256;
1488
+ const color = options?.color ?? "#000";
1489
+ if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1490
+ return renderSvg(scanned, size, color);
1491
+ }
1492
+ function asDataUrl(svg) {
1493
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1494
+ }
1495
+ function isLnAddress(destination) {
1496
+ const at = destination.indexOf("@");
1497
+ return at > 0 && destination.slice(at + 1).includes(".");
1498
+ }
1499
+ function renderSvg(content, size, color) {
1500
+ const { data } = encode(content);
1501
+ const modules = data.length;
1502
+ const cellSize = size / modules;
1503
+ let path = "";
1504
+ for (let y = 0; y < modules; y++) {
1505
+ for (let x = 0; x < modules; x++) {
1506
+ if (data[y][x]) {
1507
+ path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1508
+ }
1509
+ }
1510
+ }
1511
+ return [
1512
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1513
+ `<rect width="100%" height="100%" fill="white"/>`,
1514
+ `<path d="${path}" fill="${color}"/>`,
1515
+ `</svg>`
1516
+ ].join("");
1517
+ }
1518
+
1519
+ // src/rail.ts
1520
+ var BANK = "bank";
1521
+ var LIGHTNING = "lightning";
1522
+ function bankRail(config) {
1523
+ return async (order) => {
1524
+ const expiresAt = config.expiresAt(order);
1525
+ const transfer = await bankTransfer({
1526
+ gateway: config.gateway,
1527
+ secret: config.secret,
1528
+ iban: config.iban,
1529
+ verifyUrl: config.verifyUrl,
1530
+ reference: order.reference,
1531
+ amountMinor: order.amountMinor,
1532
+ currency: order.currency,
1533
+ expiresAt,
1534
+ trigger: config.trigger,
1535
+ sealed: await config.sealed?.(order),
1536
+ variableSymbol: config.variableSymbol?.(order),
1537
+ allowPublicGateway: config.allowPublicGateway
1538
+ });
1539
+ return {
1540
+ id: transfer.id,
1541
+ rail: config.name ?? BANK,
1542
+ scan: transfer.spd,
1543
+ qr: transfer.spd,
1544
+ expiresAt
1545
+ };
1546
+ };
1547
+ }
1548
+ function lightningRail(config) {
1549
+ return async (order) => {
1550
+ const payment = await config.gateway.createPayment(
1551
+ {
1552
+ lnAddresses: config.lnAddresses,
1553
+ amountMsat: await config.amountMsat(order),
1554
+ webhookUrl: config.webhookUrl,
1555
+ webhookSecret: config.webhookSecret
1556
+ },
1557
+ { idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
1558
+ );
1559
+ return {
1560
+ id: payment.id,
1561
+ rail: config.name ?? LIGHTNING,
1562
+ scan: payment.bolt11,
1563
+ qr: encodeForQr(payment.bolt11),
1564
+ expiresAt: payment.expiresAt
1565
+ };
1566
+ };
1567
+ }
1568
+
1630
1569
  // src/price.ts
1631
1570
  var MSAT_PER_BTC2 = 1e11;
1632
1571
  var BASIS_POINTS = 1e4;
@@ -1749,64 +1688,6 @@ function refusals2(answers) {
1749
1688
  ).join("; ");
1750
1689
  }
1751
1690
 
1752
- // src/qr.ts
1753
- import { encode } from "uqr";
1754
- var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1755
- function encodeForQr(destination) {
1756
- const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1757
- return `LIGHTNING:${scanned}`;
1758
- }
1759
- function invoiceToSvg(destination, options) {
1760
- return svgOf(encodeForQr(destination), options);
1761
- }
1762
- function invoiceToDataUrl(destination, options) {
1763
- return asDataUrl(invoiceToSvg(destination, options));
1764
- }
1765
- function lnurlToSvg(endpoint, options) {
1766
- return svgOf(toLnurl(endpoint), options);
1767
- }
1768
- function lnurlToDataUrl(endpoint, options) {
1769
- return asDataUrl(lnurlToSvg(endpoint, options));
1770
- }
1771
- function spdToSvg(spd, options) {
1772
- return svgOf(spd, options);
1773
- }
1774
- function spdToDataUrl(spd, options) {
1775
- return asDataUrl(spdToSvg(spd, options));
1776
- }
1777
- function svgOf(scanned, options) {
1778
- const size = options?.size ?? 256;
1779
- const color = options?.color ?? "#000";
1780
- if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1781
- return renderSvg(scanned, size, color);
1782
- }
1783
- function asDataUrl(svg) {
1784
- return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1785
- }
1786
- function isLnAddress(destination) {
1787
- const at = destination.indexOf("@");
1788
- return at > 0 && destination.slice(at + 1).includes(".");
1789
- }
1790
- function renderSvg(content, size, color) {
1791
- const { data } = encode(content);
1792
- const modules = data.length;
1793
- const cellSize = size / modules;
1794
- let path = "";
1795
- for (let y = 0; y < modules; y++) {
1796
- for (let x = 0; x < modules; x++) {
1797
- if (data[y][x]) {
1798
- path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1799
- }
1800
- }
1801
- }
1802
- return [
1803
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1804
- `<rect width="100%" height="100%" fill="white"/>`,
1805
- `<path d="${path}" fill="${color}"/>`,
1806
- `</svg>`
1807
- ].join("");
1808
- }
1809
-
1810
1691
  // src/webhook.ts
1811
1692
  var SIGNATURE_HEADER = "x-signature";
1812
1693
  var TIMESTAMP_HEADER = "x-timestamp";
@@ -1815,7 +1696,7 @@ var DEFAULT_TOLERANCE_SECS = 300;
1815
1696
  async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
1816
1697
  if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1817
1698
  const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
1818
- return equalInConstantTime(await sign2(secret, timestamp, body), received.toLowerCase());
1699
+ return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
1819
1700
  }
1820
1701
  async function parseWebhook(body, signature, secret, timestamp, options = {}) {
1821
1702
  if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
@@ -1837,7 +1718,7 @@ function recent(timestamp, toleranceSecs) {
1837
1718
  if (!Number.isFinite(sent)) return false;
1838
1719
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1839
1720
  }
1840
- async function sign2(secret, timestamp, body) {
1721
+ async function sign(secret, timestamp, body) {
1841
1722
  return hmacHex(secret, signed(timestamp, body));
1842
1723
  }
1843
1724
  function signed(timestamp, body) {
@@ -1860,6 +1741,7 @@ export {
1860
1741
  REQUEST_IN_FLIGHT,
1861
1742
  ThunderBridge,
1862
1743
  UnverifiedRecipientError,
1744
+ bankRail,
1863
1745
  bankTransfer,
1864
1746
  bankVerifyEndpoint,
1865
1747
  bitstamp,
@@ -1872,7 +1754,7 @@ export {
1872
1754
  isProblemType,
1873
1755
  isProvablyPaid,
1874
1756
  kraken,
1875
- lnurlPayEndpoint,
1757
+ lightningRail,
1876
1758
  lnurlToDataUrl,
1877
1759
  lnurlToSvg,
1878
1760
  medianOf,
@@ -1884,6 +1766,8 @@ export {
1884
1766
  preimageMatchesHash,
1885
1767
  proveOrigin,
1886
1768
  proveSettlement,
1769
+ qrToDataUrl,
1770
+ qrToSvg,
1887
1771
  seal,
1888
1772
  spdToDataUrl,
1889
1773
  spdToSvg,