thunder-bridge 2.1.1 → 2.2.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.
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(src_exports, {
39
39
  ThunderBridge: () => ThunderBridge,
40
40
  UnverifiedRecipientError: () => UnverifiedRecipientError,
41
41
  WrapRefusedError: () => WrapRefusedError,
42
+ agreesWithItself: () => agreesWithItself,
42
43
  answerVerifyChallenge: () => answerVerifyChallenge,
43
44
  carriesProof: () => carriesProof,
44
45
  decodeInvoice: () => decodeInvoice,
@@ -157,11 +158,11 @@ var BLOCK_BYTES = 64;
157
158
  var LENGTH_BYTES = 8;
158
159
  var SCHEDULE_WORDS = 64;
159
160
  function sha256(bytes) {
160
- const padded = withPadding(bytes);
161
- const reader = new DataView(padded.buffer);
161
+ const padded2 = withPadding(bytes);
162
+ const reader = new DataView(padded2.buffer);
162
163
  const state = Uint32Array.from(INITIAL_STATE);
163
164
  const schedule = new Uint32Array(SCHEDULE_WORDS);
164
- for (let block = 0; block < padded.length; block += BLOCK_BYTES) {
165
+ for (let block = 0; block < padded2.length; block += BLOCK_BYTES) {
165
166
  fillSchedule(schedule, reader, block);
166
167
  compress(state, schedule);
167
168
  }
@@ -171,13 +172,13 @@ function sha256Hex(text3) {
171
172
  return bytesToHex(sha256(new TextEncoder().encode(text3)));
172
173
  }
173
174
  function withPadding(bytes) {
174
- const padded = new Uint8Array(
175
+ const padded2 = new Uint8Array(
175
176
  bytes.length + 1 + LENGTH_BYTES + BLOCK_BYTES - 1 & ~(BLOCK_BYTES - 1)
176
177
  );
177
- padded.set(bytes);
178
- padded[bytes.length] = 128;
179
- new DataView(padded.buffer).setUint32(padded.length - 4, bytes.length * 8, false);
180
- return padded;
178
+ padded2.set(bytes);
179
+ padded2[bytes.length] = 128;
180
+ new DataView(padded2.buffer).setUint32(padded2.length - 4, bytes.length * 8, false);
181
+ return padded2;
181
182
  }
182
183
  function fillSchedule(schedule, reader, block) {
183
184
  for (let at = 0; at < 16; at++) {
@@ -414,63 +415,86 @@ function asBuffer(bytes) {
414
415
  // ../core/sealed.ts
415
416
  var VERSION = "v2";
416
417
  var UNPADDED_VERSION = "v1";
418
+ var PURPOSE_VERSION = "v3";
417
419
  var IV_BYTES = 12;
418
420
  var MIN_SECRET_CHARS = 32;
419
421
  var MAX_PLAIN_BYTES = 3e3;
420
422
  var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
421
423
  var END_OF_TEXT = 128;
422
- var INFO = new TextEncoder().encode("thunder-bridge/sealed");
424
+ var INFO = "thunder-bridge/sealed";
423
425
  async function seal(secret, plaintext, paymentHash) {
424
426
  return await sealUnder(
425
- secret,
427
+ await keyFor(secret, INFO),
426
428
  plaintext,
427
429
  crypto.getRandomValues(new Uint8Array(IV_BYTES)),
430
+ VERSION,
428
431
  boundTo(paymentHash)
429
432
  );
430
433
  }
431
- async function sealStable(secret, plaintext) {
434
+ async function sealFor(purpose, secret, plaintext, { stable = false } = {}) {
435
+ return await sealUnder(
436
+ await keyFor(secret, `${INFO}/${purpose}`),
437
+ plaintext,
438
+ stable ? await nonceOf(secret, plaintext) : crypto.getRandomValues(new Uint8Array(IV_BYTES)),
439
+ PURPOSE_VERSION,
440
+ new TextEncoder().encode(PURPOSE_VERSION)
441
+ );
442
+ }
443
+ async function unsealFor(purpose, secret, sealed) {
444
+ if (!sealed.startsWith(`${PURPOSE_VERSION}.`)) {
445
+ return await unseal(secret, sealed);
446
+ }
447
+ return await opened(
448
+ await keyFor(secret, `${INFO}/${purpose}`),
449
+ sealed.slice(PURPOSE_VERSION.length + 1),
450
+ new TextEncoder().encode(PURPOSE_VERSION)
451
+ );
452
+ }
453
+ async function nonceOf(secret, plaintext) {
432
454
  const derived = await hmacHex(secret, `sealed-nonce|${plaintext}`);
433
455
  const nonce = new Uint8Array(IV_BYTES);
434
456
  for (let at = 0; at < IV_BYTES; at++) {
435
457
  nonce[at] = Number.parseInt(derived.slice(at * 2, at * 2 + 2), 16);
436
458
  }
437
- return await sealUnder(secret, plaintext, nonce, boundTo(void 0));
459
+ return nonce;
438
460
  }
439
- async function sealUnder(secret, plaintext, iv, additionalData) {
461
+ async function sealUnder(key, plaintext, iv, version, additionalData) {
440
462
  const body = padToClass(plaintext);
441
- const key = await keyFor(secret);
442
463
  const cipher = new Uint8Array(
443
464
  await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
444
465
  );
445
- const joined = new Uint8Array(iv.length + cipher.length);
446
- joined.set(iv);
447
- joined.set(cipher, iv.length);
448
- return `${VERSION}.${toBase64Url(joined)}`;
466
+ const joined2 = new Uint8Array(iv.length + cipher.length);
467
+ joined2.set(iv);
468
+ joined2.set(cipher, iv.length);
469
+ return `${version}.${toBase64Url(joined2)}`;
449
470
  }
450
471
  async function unseal(secret, sealed, paymentHash) {
451
- const key = await keyFor(secret);
452
- const padded = sealed.startsWith(`${VERSION}.`);
453
- const unbound = !padded && sealed.startsWith(`${UNPADDED_VERSION}.`);
454
- if (!padded && (!unbound || paymentHash !== void 0)) {
472
+ const key = await keyFor(secret, INFO);
473
+ const padded2 = sealed.startsWith(`${VERSION}.`);
474
+ const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
475
+ if (!padded2 && (!unbound || paymentHash !== void 0)) {
455
476
  return null;
456
477
  }
457
- const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
458
- if (joined === null || joined.length <= IV_BYTES) {
478
+ return await opened(
479
+ key,
480
+ sealed.slice(VERSION.length + 1),
481
+ padded2 ? boundTo(paymentHash) : void 0
482
+ );
483
+ }
484
+ async function opened(key, encoded, additionalData) {
485
+ const joined2 = fromBase64Url(encoded);
486
+ if (joined2 === null || joined2.length <= IV_BYTES) {
459
487
  return null;
460
488
  }
461
489
  try {
462
490
  const body = new Uint8Array(
463
491
  await crypto.subtle.decrypt(
464
- {
465
- name: "AES-GCM",
466
- iv: joined.slice(0, IV_BYTES),
467
- additionalData: padded ? boundTo(paymentHash) : void 0
468
- },
492
+ { name: "AES-GCM", iv: joined2.slice(0, IV_BYTES), additionalData },
469
493
  key,
470
- joined.slice(IV_BYTES)
494
+ joined2.slice(IV_BYTES)
471
495
  )
472
496
  );
473
- const text3 = padded ? unpad(body) : body;
497
+ const text3 = additionalData === void 0 ? body : unpad(body);
474
498
  return text3 === null ? null : new TextDecoder().decode(text3);
475
499
  } catch {
476
500
  return null;
@@ -501,7 +525,7 @@ function unpad(body) {
501
525
  const end = body.lastIndexOf(END_OF_TEXT);
502
526
  return end === -1 ? null : body.subarray(0, end);
503
527
  }
504
- async function keyFor(secret) {
528
+ async function keyFor(secret, info) {
505
529
  refuseAWeakSecret(secret);
506
530
  const material = await crypto.subtle.importKey(
507
531
  "raw",
@@ -511,7 +535,12 @@ async function keyFor(secret) {
511
535
  ["deriveKey"]
512
536
  );
513
537
  return crypto.subtle.deriveKey(
514
- { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: INFO },
538
+ {
539
+ name: "HKDF",
540
+ hash: "SHA-256",
541
+ salt: new Uint8Array(0),
542
+ info: new TextEncoder().encode(info)
543
+ },
515
544
  material,
516
545
  { name: "AES-GCM", length: 256 },
517
546
  false,
@@ -944,8 +973,8 @@ function hasSmallOrder(publicKeyHex) {
944
973
  return SMALL_ORDER.includes(bytesToHex(point));
945
974
  }
946
975
  function fromBase64Url2(text3) {
947
- const padded = text3.replace(/-/g, "+").replace(/_/g, "/");
948
- const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
976
+ const padded2 = text3.replace(/-/g, "+").replace(/_/g, "/");
977
+ const raw = atob(padded2 + "=".repeat((4 - padded2.length % 4) % 4));
949
978
  const bytes = new Uint8Array(raw.length);
950
979
  for (let at = 0; at < raw.length; at++) {
951
980
  bytes[at] = raw.charCodeAt(at);
@@ -960,7 +989,9 @@ function asBuffer2(bytes) {
960
989
  var KEY_HEADER = "x-client-key";
961
990
  var SIGNATURE_HEADER = "x-signature";
962
991
  var TIMESTAMP_HEADER = "x-timestamp";
992
+ var NONCE_HEADER = "x-nonce";
963
993
  var PREFIX = "ed25519=";
994
+ var NONCE_BYTES = 16;
964
995
  var SIGNING_LABEL = "request-signing";
965
996
  async function callerKey(secret) {
966
997
  return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
@@ -968,17 +999,21 @@ async function callerKey(secret) {
968
999
  function paymentNamedBy(caller, paymentHash) {
969
1000
  return sha256Hex(`${caller}.payment-id.${paymentHash}`);
970
1001
  }
971
- async function signedAs(key, method, path, body) {
1002
+ async function signedAs(key, method, path, body, host) {
972
1003
  const timestamp = String(Math.floor(Date.now() / 1e3));
1004
+ const nonce = bytesToHex(crypto.getRandomValues(new Uint8Array(NONCE_BYTES)));
973
1005
  return {
974
1006
  [KEY_HEADER]: key.publicKeyHex,
975
1007
  [TIMESTAMP_HEADER]: timestamp,
976
- [SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
1008
+ [NONCE_HEADER]: nonce,
1009
+ [SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spokenTo(host, timestamp, nonce, method, path, body))}`
977
1010
  };
978
1011
  }
979
- function spoken(timestamp, method, path, body) {
1012
+ function spokenTo(host, timestamp, nonce, method, path, body) {
980
1013
  return new TextEncoder().encode(
981
- `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
1014
+ ["v2", host.toLowerCase(), timestamp, nonce, method.toUpperCase(), path, sha256Hex(body)].join(
1015
+ "\n"
1016
+ )
982
1017
  );
983
1018
  }
984
1019
 
@@ -1160,14 +1195,39 @@ async function addressesToReach(url) {
1160
1195
  return found;
1161
1196
  }
1162
1197
  async function resolved(url) {
1163
- const { lookup } = await import("dns/promises");
1164
- const { setTimeout: sleep } = await import("timers/promises");
1198
+ const { isIP } = await import("net");
1165
1199
  const host = new URL(url).hostname.replace(/^\[|]$/g, "");
1200
+ const literal = isIP(host);
1201
+ if (literal !== 0) {
1202
+ return [{ address: host, family: literal }];
1203
+ }
1204
+ const { Resolver } = await import("dns/promises");
1205
+ const { setTimeout: sleep } = await import("timers/promises");
1206
+ const resolver = new Resolver({ timeout: LOOKUP_TIMEOUT_MS, tries: 1 });
1166
1207
  const found = await Promise.race([
1167
- lookup(host, { all: true, verbatim: true }).catch(() => []),
1208
+ Promise.all([
1209
+ resolver.resolve4(host).catch(() => []),
1210
+ resolver.resolve6(host).catch(() => [])
1211
+ ]),
1168
1212
  sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
1169
1213
  ]);
1170
- return found ?? [];
1214
+ cancelled(resolver);
1215
+ if (found === null) {
1216
+ return [];
1217
+ }
1218
+ const [four, six] = found;
1219
+ return [
1220
+ ...four.map((address) => ({ address, family: 4 })),
1221
+ ...six.map((address) => ({ address, family: 6 }))
1222
+ ];
1223
+ }
1224
+ function cancelled(resolver) {
1225
+ try {
1226
+ resolver.cancel();
1227
+ return true;
1228
+ } catch {
1229
+ return false;
1230
+ }
1171
1231
  }
1172
1232
  async function answerOf(response) {
1173
1233
  const { status, ok, headers } = response;
@@ -1496,6 +1556,14 @@ function headersOf(raw) {
1496
1556
  return headers;
1497
1557
  }
1498
1558
 
1559
+ // ../core/delivery.ts
1560
+ function deliverySigned(url, timestamp, body) {
1561
+ const endpoint = new URL(url);
1562
+ return new TextEncoder().encode(
1563
+ ["v2", `${endpoint.origin}${endpoint.pathname}`, timestamp, body].join("\n")
1564
+ );
1565
+ }
1566
+
1499
1567
  // src/verify.ts
1500
1568
  var HTTP_TIMEOUT_MS2 = 15e3;
1501
1569
  var ISSUED_SKEW_SECS = 300;
@@ -1567,7 +1635,7 @@ async function proveSettlement(payment, asked) {
1567
1635
  }
1568
1636
  return verified.preimage;
1569
1637
  }
1570
- function carriesProof(report) {
1638
+ function agreesWithItself(report) {
1571
1639
  if (report.status !== "paid" || report.preimage === null) {
1572
1640
  return false;
1573
1641
  }
@@ -1579,6 +1647,7 @@ function carriesProof(report) {
1579
1647
  }
1580
1648
  return preimageMatchesHash(report.preimage, report.paymentHash);
1581
1649
  }
1650
+ var carriesProof = agreesWithItself;
1582
1651
  function wrapFeeCeiling(amountMsat, allowance) {
1583
1652
  const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
1584
1653
  return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
@@ -1860,7 +1929,7 @@ function isStatus(value) {
1860
1929
  }
1861
1930
 
1862
1931
  // src/webhook.ts
1863
- var SIGNATURE_HEADER2 = "x-signature";
1932
+ var SIGNATURE_HEADER2 = "x-signature-v2";
1864
1933
  var TIMESTAMP_HEADER2 = "x-timestamp";
1865
1934
  var GATEWAY_KEY_PREFIX = "ed25519=";
1866
1935
  var DEFAULT_TOLERANCE_SECS = 300;
@@ -1876,14 +1945,14 @@ async function readSettlement(request, credential, options = {}) {
1876
1945
  }
1877
1946
  async function answerWebhookChallenge(request, credential, options = {}) {
1878
1947
  const body = await believable(request, credential, options);
1879
- const nonce = body === null ? null : nonceOf(body, CHALLENGE);
1948
+ const nonce = body === null ? null : nonceOf2(body, CHALLENGE);
1880
1949
  return nonce === null ? null : answered2(nonce);
1881
1950
  }
1882
1951
  async function answerVerifyChallenge(request) {
1883
1952
  if (request.method !== "POST") {
1884
1953
  return null;
1885
1954
  }
1886
- const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
1955
+ const nonce = nonceOf2(await request.clone().text(), VERIFY_CHALLENGE);
1887
1956
  return nonce === null ? null : answered2(nonce);
1888
1957
  }
1889
1958
  async function believable(request, credential, options) {
@@ -1902,12 +1971,12 @@ async function believable(request, credential, options) {
1902
1971
  const signed = await verifyHex(
1903
1972
  credential.publicKey.toLowerCase(),
1904
1973
  signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1905
- stamped(timestamp, body)
1974
+ deliverySigned(options.url ?? request.url, timestamp, body)
1906
1975
  );
1907
1976
  return signed ? body : null;
1908
1977
  }
1909
1978
  function proved(read) {
1910
- return read !== null && read.status === "paid" && !carriesProof(read) ? null : read;
1979
+ return read !== null && read.status === "paid" && !agreesWithItself(read) ? null : read;
1911
1980
  }
1912
1981
  function decoded(body, from) {
1913
1982
  try {
@@ -1921,7 +1990,7 @@ function answered2(nonce) {
1921
1990
  headers: { "content-type": "application/json" }
1922
1991
  });
1923
1992
  }
1924
- function nonceOf(text3, type) {
1993
+ function nonceOf2(text3, type) {
1925
1994
  try {
1926
1995
  const said = JSON.parse(text3);
1927
1996
  if (said["type"] !== type || typeof said["nonce"] !== "string") {
@@ -1939,9 +2008,6 @@ function recent(timestamp, toleranceSecs) {
1939
2008
  }
1940
2009
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1941
2010
  }
1942
- function stamped(timestamp, body) {
1943
- return new TextEncoder().encode(`${timestamp}.${body}`);
1944
- }
1945
2011
 
1946
2012
  // src/bank.ts
1947
2013
  var DEFAULT_CURRENCY = "CZK";
@@ -1983,7 +2049,7 @@ function bankVerifyEndpoint(config) {
1983
2049
  throw new Error(`${config.iban} is not an IBAN, so this endpoint answers for no account`);
1984
2050
  }
1985
2051
  const paced = {
1986
- "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
2052
+ "cache-control": `max-age=${config.pollEverySecs ?? config.statement.freshEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1987
2053
  };
1988
2054
  return async (request) => {
1989
2055
  const consented = await answerVerifyChallenge(request);
@@ -1994,7 +2060,7 @@ function bankVerifyEndpoint(config) {
1994
2060
  if (sealed === null) {
1995
2061
  return Response.json({ settled: false }, { status: 400 });
1996
2062
  }
1997
- const subject = await unseal(config.secret, sealed);
2063
+ const subject = await unsealFor("bank-verify", config.secret, sealed);
1998
2064
  const asked = subject === null ? null : askedFrom(subject);
1999
2065
  if (asked === null || asked.iban !== answersFor) {
2000
2066
  return Response.json({ settled: false }, { status: 403 });
@@ -2013,7 +2079,10 @@ async function answeredAt(params, subject) {
2013
2079
  return `agent:${(await callerKey(params.secret)).publicKeyHex}`;
2014
2080
  }
2015
2081
  const polling = new URL(params.verifyUrl ?? "");
2016
- polling.searchParams.set("q", await sealStable(params.secret, subject));
2082
+ polling.searchParams.set(
2083
+ "q",
2084
+ await sealFor("bank-verify", params.secret, subject, { stable: true })
2085
+ );
2017
2086
  return polling.toString();
2018
2087
  }
2019
2088
  async function creditedPreimage(params) {
@@ -2119,6 +2188,161 @@ function unixNow() {
2119
2188
  return Math.floor(Date.now() / 1e3);
2120
2189
  }
2121
2190
 
2191
+ // src/nostr.ts
2192
+ var import_chacha = require("@noble/ciphers/chacha.js");
2193
+ var import_secp256k1 = require("@noble/curves/secp256k1.js");
2194
+ var import_hkdf = require("@noble/hashes/hkdf.js");
2195
+ var import_hmac4 = require("@noble/hashes/hmac.js");
2196
+ var import_sha2 = require("@noble/hashes/sha2.js");
2197
+ var NIP44_SALT = new TextEncoder().encode("nip44-v2");
2198
+ var NIP44_VERSION = 2;
2199
+ var NONCE_BYTES2 = 32;
2200
+ var MAC_BYTES = 32;
2201
+ var MESSAGE_KEYS_BYTES = 76;
2202
+ var CHACHA_KEY_END = 32;
2203
+ var CHACHA_NONCE_END = 44;
2204
+ var LENGTH_PREFIX_BYTES = 2;
2205
+ var SMALLEST_PAD = 32;
2206
+ var CHUNKS_STAY_SMALL_TO = 256;
2207
+ var MAX_PLAINTEXT_BYTES = 65535;
2208
+ function publicKeyFor(privateKeyHex) {
2209
+ return bytesToHex(import_secp256k1.schnorr.getPublicKey(hexToBytes(privateKeyHex)));
2210
+ }
2211
+ function conversationKeyFor(privateKeyHex, publicKeyHex) {
2212
+ const shared = import_secp256k1.secp256k1.getSharedSecret(
2213
+ hexToBytes(privateKeyHex),
2214
+ hexToBytes(`02${publicKeyHex}`)
2215
+ );
2216
+ return (0, import_hkdf.extract)(import_sha2.sha256, shared.subarray(1, 33), NIP44_SALT);
2217
+ }
2218
+ function signEvent(privateKeyHex, draft) {
2219
+ const pubkey = publicKeyFor(privateKeyHex);
2220
+ const id = idOf(pubkey, draft);
2221
+ return {
2222
+ id,
2223
+ pubkey,
2224
+ created_at: draft.created_at,
2225
+ kind: draft.kind,
2226
+ tags: draft.tags,
2227
+ content: draft.content,
2228
+ sig: bytesToHex(import_secp256k1.schnorr.sign(hexToBytes(id), hexToBytes(privateKeyHex)))
2229
+ };
2230
+ }
2231
+ function verifyEvent(event) {
2232
+ try {
2233
+ return idOf(event.pubkey, event) === event.id && import_secp256k1.schnorr.verify(hexToBytes(event.sig), hexToBytes(event.id), hexToBytes(event.pubkey));
2234
+ } catch {
2235
+ return false;
2236
+ }
2237
+ }
2238
+ function idOf(pubkey, draft) {
2239
+ return bytesToHex(
2240
+ (0, import_sha2.sha256)(
2241
+ new TextEncoder().encode(
2242
+ JSON.stringify([0, pubkey, draft.created_at, draft.kind, draft.tags, draft.content])
2243
+ )
2244
+ )
2245
+ );
2246
+ }
2247
+ function encryptNip44(conversationKey, plaintext) {
2248
+ return sealNip44(conversationKey, plaintext, crypto.getRandomValues(new Uint8Array(NONCE_BYTES2)));
2249
+ }
2250
+ function sealNip44(conversationKey, plaintext, nonce) {
2251
+ const keys = messageKeysFrom(conversationKey, nonce);
2252
+ const ciphertext = (0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, padded(plaintext));
2253
+ const mac = (0, import_hmac4.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext));
2254
+ return toBase64(joined(Uint8Array.of(NIP44_VERSION), nonce, ciphertext, mac));
2255
+ }
2256
+ function decryptNip44(conversationKey, payload) {
2257
+ if (payload.startsWith("#")) {
2258
+ throw new Error("nip44 payload announces an encryption version we do not speak");
2259
+ }
2260
+ const raw = fromBase64(payload);
2261
+ if (raw.length < 1 + NONCE_BYTES2 + MAC_BYTES) {
2262
+ throw new Error(`nip44 payload is ${raw.length} bytes, too short to hold a nonce and a mac`);
2263
+ }
2264
+ if (raw[0] !== NIP44_VERSION) {
2265
+ throw new Error(`nip44 payload is version ${raw[0]}, not ${NIP44_VERSION}`);
2266
+ }
2267
+ const nonce = raw.subarray(1, 1 + NONCE_BYTES2);
2268
+ const ciphertext = raw.subarray(1 + NONCE_BYTES2, raw.length - MAC_BYTES);
2269
+ const mac = raw.subarray(raw.length - MAC_BYTES);
2270
+ const keys = messageKeysFrom(conversationKey, nonce);
2271
+ if (!sameBytes((0, import_hmac4.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext)), mac)) {
2272
+ throw new Error("nip44 payload failed its mac, so it was not written with this key");
2273
+ }
2274
+ return unpadded((0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, ciphertext));
2275
+ }
2276
+ function messageKeysFrom(conversationKey, nonce) {
2277
+ if (conversationKey.length !== NONCE_BYTES2 || nonce.length !== NONCE_BYTES2) {
2278
+ throw new Error("a nip44 conversation key and nonce are both 32 bytes");
2279
+ }
2280
+ const keys = (0, import_hkdf.expand)(import_sha2.sha256, conversationKey, nonce, MESSAGE_KEYS_BYTES);
2281
+ return {
2282
+ chachaKey: keys.subarray(0, CHACHA_KEY_END),
2283
+ chachaNonce: keys.subarray(CHACHA_KEY_END, CHACHA_NONCE_END),
2284
+ hmacKey: keys.subarray(CHACHA_NONCE_END)
2285
+ };
2286
+ }
2287
+ function paddedLengthFor(unpadded2) {
2288
+ if (unpadded2 <= SMALLEST_PAD) {
2289
+ return SMALLEST_PAD;
2290
+ }
2291
+ const nextPower = 1 << Math.floor(Math.log2(unpadded2 - 1)) + 1;
2292
+ const chunk = nextPower <= CHUNKS_STAY_SMALL_TO ? SMALLEST_PAD : nextPower / 8;
2293
+ return chunk * (Math.floor((unpadded2 - 1) / chunk) + 1);
2294
+ }
2295
+ function padded(plaintext) {
2296
+ const body = new TextEncoder().encode(plaintext);
2297
+ if (body.length === 0 || body.length > MAX_PLAINTEXT_BYTES) {
2298
+ throw new Error(`nip44 takes 1 to ${MAX_PLAINTEXT_BYTES} bytes, this was ${body.length}`);
2299
+ }
2300
+ const buffer = new Uint8Array(LENGTH_PREFIX_BYTES + paddedLengthFor(body.length));
2301
+ buffer.set(Uint8Array.of(body.length >>> 8, body.length & 255));
2302
+ buffer.set(body, LENGTH_PREFIX_BYTES);
2303
+ return buffer;
2304
+ }
2305
+ function unpadded(buffer) {
2306
+ const length = (buffer[0] ?? 0) << 8 | (buffer[1] ?? 0);
2307
+ if (length === 0 || buffer.length !== LENGTH_PREFIX_BYTES + paddedLengthFor(length)) {
2308
+ throw new Error("nip44 plaintext does not match the length its own padding declares");
2309
+ }
2310
+ return new TextDecoder().decode(
2311
+ buffer.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + length)
2312
+ );
2313
+ }
2314
+ function joined(...parts) {
2315
+ const whole = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
2316
+ let at = 0;
2317
+ for (const part of parts) {
2318
+ whole.set(part, at);
2319
+ at += part.length;
2320
+ }
2321
+ return whole;
2322
+ }
2323
+ function sameBytes(left, right) {
2324
+ let different = left.length ^ right.length;
2325
+ for (let at = 0; at < left.length && at < right.length; at++) {
2326
+ different |= (left[at] ?? 0) ^ (right[at] ?? 0);
2327
+ }
2328
+ return different === 0;
2329
+ }
2330
+ function toBase64(bytes) {
2331
+ let binary = "";
2332
+ for (const byte of bytes) {
2333
+ binary += String.fromCharCode(byte);
2334
+ }
2335
+ return btoa(binary);
2336
+ }
2337
+ function fromBase64(text3) {
2338
+ const binary = atob(text3);
2339
+ const bytes = new Uint8Array(binary.length);
2340
+ for (let at = 0; at < bytes.length; at++) {
2341
+ bytes[at] = binary.charCodeAt(at);
2342
+ }
2343
+ return bytes;
2344
+ }
2345
+
2122
2346
  // src/qr.ts
2123
2347
  var import_uqr = require("uqr");
2124
2348
  var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
@@ -2161,12 +2385,244 @@ function renderSvg(content, size, color) {
2161
2385
  ].join("");
2162
2386
  }
2163
2387
 
2164
- // src/relay.ts
2388
+ // src/nwc.ts
2389
+ var REQUEST_KIND = 23194;
2390
+ var RESPONSE_KIND = 23195;
2391
+ var NIP44_V2 = "nip44_v2";
2392
+ var DEFAULT_ASK_TIMEOUT_MS = 1e4;
2165
2393
  var DEFAULT_POLL_EVERY_SECS2 = 5;
2394
+ var HASH = "h";
2395
+ var REFUSAL_BY_CODE = {
2396
+ INSUFFICIENT_BALANCE: "amount-not-accepted",
2397
+ QUOTA_EXCEEDED: "amount-not-accepted",
2398
+ RESTRICTED: "address-unusable",
2399
+ UNAUTHORIZED: "address-unusable",
2400
+ UNSUPPORTED_ENCRYPTION: "address-unusable"
2401
+ };
2402
+ async function nwcInvoice(connection, amountMsat, description, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
2403
+ const answered3 = await askWallet(
2404
+ connection,
2405
+ "make_invoice",
2406
+ { amount: amountMsat, description },
2407
+ timeoutMs
2408
+ );
2409
+ const bolt11 = String(answered3.invoice ?? "");
2410
+ const issued = decodeInvoice(bolt11);
2411
+ if (issued.paymentHash === null || issued.expiresAt === null) {
2412
+ throw new WalletRefused("invoice-refused", "the nwc wallet issued an invoice we cannot decode");
2413
+ }
2414
+ if (issued.amountMsat !== amountMsat) {
2415
+ throw new WalletRefused(
2416
+ "invoice-refused",
2417
+ `the nwc wallet issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
2418
+ );
2419
+ }
2420
+ return { bolt11, paymentHash: issued.paymentHash, expiresAt: issued.expiresAt };
2421
+ }
2422
+ async function nwcSettlement(connection, paymentHash, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
2423
+ const answered3 = await askWallet(
2424
+ connection,
2425
+ "lookup_invoice",
2426
+ { payment_hash: paymentHash },
2427
+ timeoutMs
2428
+ );
2429
+ const preimage = answered3.preimage;
2430
+ if (typeof preimage !== "string" || preimage.length === 0) {
2431
+ return null;
2432
+ }
2433
+ if (!preimageMatchesHash(preimage, paymentHash)) {
2434
+ throw new Error(`the nwc wallet returned a preimage that does not hash to ${paymentHash}`);
2435
+ }
2436
+ return preimage;
2437
+ }
2438
+ function nwcVerifyEndpoint(config) {
2439
+ const paced = {
2440
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
2441
+ };
2442
+ return async (request) => {
2443
+ const consented = await answerVerifyChallenge(request);
2444
+ if (consented !== null) {
2445
+ return consented;
2446
+ }
2447
+ const sealed = new URL(request.url).searchParams.get(HASH);
2448
+ if (sealed === null) {
2449
+ return Response.json({ settled: false }, { status: 400 });
2450
+ }
2451
+ const paymentHash = await unsealFor("nwc-verify", config.secret, sealed);
2452
+ if (paymentHash === null) {
2453
+ return Response.json({ settled: false }, { status: 403 });
2454
+ }
2455
+ const preimage = await nwcSettlement(config.connection, paymentHash, config.askTimeoutMs).catch(
2456
+ () => void 0
2457
+ );
2458
+ if (preimage === void 0) {
2459
+ return Response.json({ settled: false }, { status: 502 });
2460
+ }
2461
+ return Response.json({ settled: preimage !== null, preimage }, { headers: paced });
2462
+ };
2463
+ }
2464
+ async function nwcVerifyUrl(endpoint, paymentHash, secret) {
2465
+ const sealed = new URL(endpoint);
2466
+ sealed.searchParams.set(HASH, await sealFor("nwc-verify", secret, paymentHash));
2467
+ return sealed.toString();
2468
+ }
2469
+ async function askWallet(connection, method, params, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
2470
+ const key = conversationKeyFor(connection.secret, connection.walletPubkey);
2471
+ const request = signEvent(connection.secret, {
2472
+ kind: REQUEST_KIND,
2473
+ tags: [
2474
+ ["p", connection.walletPubkey],
2475
+ ["encryption", NIP44_V2]
2476
+ ],
2477
+ content: encryptNip44(key, JSON.stringify({ method, params })),
2478
+ created_at: Math.floor(Date.now() / 1e3)
2479
+ });
2480
+ const answer = await overFirstRelayThatAnswers(connection, request, timeoutMs);
2481
+ return resultOf(JSON.parse(decryptNip44(key, answer.content)), method);
2482
+ }
2483
+ function resultOf(said, method) {
2484
+ const failure = said.error;
2485
+ if (failure) {
2486
+ throw new WalletRefused(
2487
+ REFUSAL_BY_CODE[failure.code ?? ""] ?? "invoice-refused",
2488
+ `the nwc wallet refused ${method}: ${failure.code ?? "unknown"} ${failure.message ?? ""}`.trim()
2489
+ );
2490
+ }
2491
+ const result = said.result;
2492
+ if (result === null || typeof result !== "object") {
2493
+ throw new WalletRefused("invoice-refused", `the nwc wallet answered ${method} with no result`);
2494
+ }
2495
+ return result;
2496
+ }
2497
+ async function overFirstRelayThatAnswers(connection, request, timeoutMs) {
2498
+ const deadline = AbortSignal.timeout(timeoutMs);
2499
+ let lastFailure = null;
2500
+ for (const relay of connection.relays) {
2501
+ if (deadline.aborted) {
2502
+ break;
2503
+ }
2504
+ try {
2505
+ return await overRelay(relay, connection, request, deadline);
2506
+ } catch (failure) {
2507
+ lastFailure = failure;
2508
+ }
2509
+ }
2510
+ throw new WalletRefused("unreachable", `no nwc relay answered: ${String(lastFailure)}`);
2511
+ }
2512
+ function overRelay(relay, connection, request, deadline) {
2513
+ return new Promise((resolve2, reject) => {
2514
+ const socket = new WebSocket(relay);
2515
+ const subscription = request.id.slice(0, 16);
2516
+ const expired = () => {
2517
+ close();
2518
+ reject(new Error(`${relay} did not answer before the deadline`));
2519
+ };
2520
+ deadline.addEventListener("abort", expired);
2521
+ function close() {
2522
+ deadline.removeEventListener("abort", expired);
2523
+ socket.close();
2524
+ }
2525
+ socket.addEventListener("open", () => {
2526
+ socket.send(
2527
+ JSON.stringify([
2528
+ "REQ",
2529
+ subscription,
2530
+ {
2531
+ kinds: [RESPONSE_KIND],
2532
+ authors: [connection.walletPubkey],
2533
+ "#p": [publicKeyFor(connection.secret)],
2534
+ "#e": [request.id]
2535
+ }
2536
+ ])
2537
+ );
2538
+ socket.send(JSON.stringify(["EVENT", request]));
2539
+ });
2540
+ socket.addEventListener("error", () => {
2541
+ close();
2542
+ reject(new Error(`${relay} could not be reached`));
2543
+ });
2544
+ socket.addEventListener("message", (delivered) => {
2545
+ const frame = String(delivered.data);
2546
+ const rejection = rejectionOf(frame, request.id);
2547
+ if (rejection !== null) {
2548
+ close();
2549
+ reject(new Error(`${relay} refused the request: ${rejection}`));
2550
+ return;
2551
+ }
2552
+ const answer = answeredEvent(frame, subscription, connection, request.id);
2553
+ if (answer === null) {
2554
+ return;
2555
+ }
2556
+ close();
2557
+ resolve2(answer);
2558
+ });
2559
+ });
2560
+ }
2561
+ function rejectionOf(frame, requestId) {
2562
+ const parsed = parsedFrame(frame);
2563
+ if (parsed === null || parsed[0] !== "OK" || parsed[1] !== requestId || parsed[2] !== false) {
2564
+ return null;
2565
+ }
2566
+ return typeof parsed[3] === "string" && parsed[3].length > 0 ? parsed[3] : "no reason given";
2567
+ }
2568
+ function answeredEvent(frame, subscription, connection, requestId) {
2569
+ const parsed = parsedFrame(frame);
2570
+ if (parsed === null || parsed[0] !== "EVENT" || parsed[1] !== subscription) {
2571
+ return null;
2572
+ }
2573
+ const event = parsed[2];
2574
+ const answers = event?.kind === RESPONSE_KIND && event.pubkey === connection.walletPubkey && Array.isArray(event.tags) && event.tags.some((tag) => tag?.[0] === "e" && tag[1] === requestId) && verifyEvent(event);
2575
+ return answers ? event : null;
2576
+ }
2577
+ function parsedFrame(frame) {
2578
+ try {
2579
+ const parsed = JSON.parse(frame);
2580
+ return Array.isArray(parsed) ? parsed : null;
2581
+ } catch {
2582
+ return null;
2583
+ }
2584
+ }
2585
+ function nwcRail(gateway, config) {
2586
+ return async (order) => {
2587
+ const invoice = await nwcInvoice(
2588
+ config.connection,
2589
+ await msatForOrder(order, config.amount, config.rate),
2590
+ config.description?.(order) ?? order.reference
2591
+ );
2592
+ const watched = await gateway.watch({
2593
+ paymentHash: invoice.paymentHash,
2594
+ verifyUrl: await nwcVerifyUrl(
2595
+ config.verifyThrough.endpoint,
2596
+ invoice.paymentHash,
2597
+ config.verifyThrough.secret
2598
+ ),
2599
+ expiresAt: invoice.expiresAt,
2600
+ trigger: config.trigger,
2601
+ replay: config.replay,
2602
+ sealed: config.sealed ? await seal(
2603
+ config.sealed.secret,
2604
+ JSON.stringify(config.sealed.data(order)),
2605
+ invoice.paymentHash
2606
+ ) : void 0,
2607
+ webhookUrl: config.webhookUrl
2608
+ });
2609
+ return {
2610
+ id: watched.id,
2611
+ paymentHash: invoice.paymentHash,
2612
+ rail: config.name ?? "lightning",
2613
+ scan: invoice.bolt11,
2614
+ qr: toLightningUri(invoice.bolt11),
2615
+ expiresAt: invoice.expiresAt
2616
+ };
2617
+ };
2618
+ }
2619
+
2620
+ // src/relay.ts
2621
+ var DEFAULT_POLL_EVERY_SECS3 = 5;
2166
2622
  var WALLET = "w";
2167
2623
  function lightningVerifyEndpoint(config) {
2168
2624
  const paced = {
2169
- "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
2625
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS3}`
2170
2626
  };
2171
2627
  return async (request) => {
2172
2628
  const consented = await answerVerifyChallenge(request);
@@ -2177,8 +2633,8 @@ function lightningVerifyEndpoint(config) {
2177
2633
  if (sealed === null) {
2178
2634
  return Response.json({ settled: false }, { status: 400 });
2179
2635
  }
2180
- const opened = await unseal(config.secret, sealed);
2181
- const wallet = opened === null ? null : relayedFrom(opened);
2636
+ const opened2 = await unsealFor("relay", config.secret, sealed);
2637
+ const wallet = opened2 === null ? null : relayedFrom(opened2);
2182
2638
  if (wallet === null) {
2183
2639
  return Response.json({ settled: false }, { status: 403 });
2184
2640
  }
@@ -2196,9 +2652,9 @@ function lightningVerifyEndpoint(config) {
2196
2652
  );
2197
2653
  };
2198
2654
  }
2199
- function relayedFrom(opened) {
2655
+ function relayedFrom(opened2) {
2200
2656
  try {
2201
- const said = JSON.parse(opened);
2657
+ const said = JSON.parse(opened2);
2202
2658
  return typeof said?.url === "string" && typeof said.hash === "string" ? { url: said.url, hash: said.hash } : null;
2203
2659
  } catch {
2204
2660
  return null;
@@ -2206,7 +2662,7 @@ function relayedFrom(opened) {
2206
2662
  }
2207
2663
  async function relayedVerifyUrl(endpoint, wallet, secret) {
2208
2664
  const relayed = new URL(endpoint);
2209
- relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
2665
+ relayed.searchParams.set(WALLET, await sealFor("relay", secret, JSON.stringify(wallet)));
2210
2666
  return relayed.toString();
2211
2667
  }
2212
2668
 
@@ -2337,6 +2793,15 @@ var Rails = class {
2337
2793
  bank(config) {
2338
2794
  return bankRail(this.gateway, config);
2339
2795
  }
2796
+ /**
2797
+ * Lightning against a wallet of your own over NIP-47, for a wallet that has no
2798
+ * LUD-21 address to be watched at. Your node mints the invoice and releases the
2799
+ * preimage, so the proof comes from one hop nearer than any hosted address can
2800
+ * manage, and the gateway sees a hash and a URL of yours
2801
+ */
2802
+ nwc(config) {
2803
+ return nwcRail(this.gateway, config);
2804
+ }
2340
2805
  /**
2341
2806
  * One bank transfer without building a rail first, for a shop that asks for
2342
2807
  * them one at a time rather than beside another payment method
@@ -2386,7 +2851,7 @@ function waitingOf(options) {
2386
2851
  }
2387
2852
 
2388
2853
  // src/trigger.ts
2389
- var NONCE_BYTES = 16;
2854
+ var NONCE_BYTES3 = 16;
2390
2855
  function lnurlPayEndpoint(gateway, config) {
2391
2856
  return async (request) => {
2392
2857
  const url = new URL(request.url);
@@ -2525,10 +2990,10 @@ function alreadyWatched(refused) {
2525
2990
  return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
2526
2991
  }
2527
2992
  function sign(secret, address, least, most, nonce) {
2528
- return hmacHex(secret, `${address}|${least}|${most}|${nonce}`);
2993
+ return hmacHex(secret, `lnurl-callback|${address}|${least}|${most}|${nonce}`);
2529
2994
  }
2530
2995
  function randomNonce() {
2531
- const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
2996
+ const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES3));
2532
2997
  let hex = "";
2533
2998
  for (const byte of bytes) {
2534
2999
  hex += byte.toString(16).padStart(2, "0");
@@ -2578,23 +3043,39 @@ var Serve = class {
2578
3043
  * A verify endpoint of your own that asks the recipient's wallet for you, so
2579
3044
  * the gateway polls you and never the wallet
2580
3045
  */
2581
- verify(config) {
3046
+ lightningVerify(config) {
2582
3047
  return lightningVerifyEndpoint(config);
2583
3048
  }
3049
+ /**
3050
+ * What `lightningVerify` was called before 2.2.0
3051
+ *
3052
+ * @deprecated Use `lightningVerify`, beside `bankVerify` and `nwcVerify`
3053
+ */
3054
+ verify(config) {
3055
+ return this.lightningVerify(config);
3056
+ }
2584
3057
  /** The verify endpoint a bank rail is polled at, answering off your own statement */
2585
3058
  bankVerify(config) {
2586
3059
  return bankVerifyEndpoint(config);
2587
3060
  }
3061
+ /**
3062
+ * The verify endpoint an NWC rail is polled at, asking your own wallet over
3063
+ * NIP-47, so the gateway never learns the connection, the relay or the wallet
3064
+ */
3065
+ nwcVerify(config) {
3066
+ return nwcVerifyEndpoint(config);
3067
+ }
2588
3068
  /**
2589
3069
  * The whole webhook route: it answers the gateway's challenge, checks the
2590
3070
  * signature against the key the gateway publishes, refuses a settlement that
2591
- * proves nothing, and calls you for the one that does.
3071
+ * proves nothing, and calls you once for the one that does.
2592
3072
  *
2593
3073
  * `export const POST = gateway.serve.webhook({ onSettled: fulfil })` is the
2594
3074
  * entire integration
2595
3075
  */
2596
3076
  webhook(handlers) {
2597
- const options = { toleranceSecs: handlers.toleranceSecs };
3077
+ const options = { toleranceSecs: handlers.toleranceSecs, url: handlers.url };
3078
+ const once = onceWhileReplayable(handlers.toleranceSecs ?? DEFAULT_TOLERANCE_SECS);
2598
3079
  return async (request) => {
2599
3080
  const credential = await this.credential(handlers);
2600
3081
  const challenge = await answerWebhookChallenge(request, credential, options);
@@ -2603,12 +3084,17 @@ var Serve = class {
2603
3084
  }
2604
3085
  const settlement = await readSettlement(request, credential, options);
2605
3086
  if (settlement !== null) {
2606
- return await acted(settlement, handlers);
3087
+ return await once(
3088
+ `${settlement.id}:${settlement.status}`,
3089
+ () => acted(settlement, handlers)
3090
+ );
2607
3091
  }
2608
3092
  const payment = await readPayment(request, credential, options);
2609
3093
  if (payment !== null) {
2610
- await handlers.onPayment?.(payment);
2611
- return new Response("ok");
3094
+ return await once(`${payment.id}:${payment.status}`, async () => {
3095
+ await handlers.onPayment?.(payment);
3096
+ return new Response("ok");
3097
+ });
2612
3098
  }
2613
3099
  return new Response("bad signature", { status: 401 });
2614
3100
  };
@@ -2629,8 +3115,34 @@ var Serve = class {
2629
3115
  return handlers.credential ?? { publicKey: await this.gateway.webhookKey() };
2630
3116
  }
2631
3117
  };
3118
+ function onceWhileReplayable(toleranceSecs) {
3119
+ const acting = /* @__PURE__ */ new Map();
3120
+ const replayableForMs = 2 * toleranceSecs * 1e3;
3121
+ return async (delivery, act) => {
3122
+ const now = Date.now();
3123
+ for (const [earlier2, { forgetAt }] of acting) {
3124
+ if (forgetAt > now) {
3125
+ break;
3126
+ }
3127
+ acting.delete(earlier2);
3128
+ }
3129
+ const earlier = acting.get(delivery);
3130
+ if (earlier !== void 0) {
3131
+ await earlier.done;
3132
+ return new Response("ok");
3133
+ }
3134
+ const done = act();
3135
+ acting.set(delivery, { forgetAt: now + replayableForMs, done });
3136
+ try {
3137
+ return await done;
3138
+ } catch (failure) {
3139
+ acting.delete(delivery);
3140
+ throw failure;
3141
+ }
3142
+ };
3143
+ }
2632
3144
  async function acted(settlement, handlers) {
2633
- if (carriesProof(settlement)) {
3145
+ if (agreesWithItself(settlement)) {
2634
3146
  await handlers.onSettled?.(settlement);
2635
3147
  return new Response("ok");
2636
3148
  }
@@ -2854,7 +3366,7 @@ var ThunderBridge = class {
2854
3366
  throw await problemFrom(response);
2855
3367
  }
2856
3368
  const body = await response.json().catch(() => null);
2857
- const listed2 = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null).filter((one) => one.status !== "paid" || carriesProof(one)) : null;
3369
+ const listed2 = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null).filter((one) => one.status !== "paid" || agreesWithItself(one)) : null;
2858
3370
  if (listed2 === null || typeof body?.settled_scanned !== "number") {
2859
3371
  throw new ProblemError({
2860
3372
  status: response.status,
@@ -2939,9 +3451,9 @@ var ThunderBridge = class {
2939
3451
  if (settled) {
2940
3452
  return;
2941
3453
  }
2942
- const opened = new WebSocket(url);
2943
- socket = opened;
2944
- opened.onmessage = (event) => {
3454
+ const opened2 = new WebSocket(url);
3455
+ socket = opened2;
3456
+ opened2.onmessage = (event) => {
2945
3457
  try {
2946
3458
  const frame = JSON.parse(String(event.data));
2947
3459
  const watched = paymentFromWire(frame);
@@ -2958,7 +3470,7 @@ var ThunderBridge = class {
2958
3470
  settle(() => reject(refused));
2959
3471
  }
2960
3472
  };
2961
- opened.onclose = again;
3473
+ opened2.onclose = again;
2962
3474
  };
2963
3475
  options?.signal?.addEventListener("abort", abort, { once: true });
2964
3476
  void connect();
@@ -2981,6 +3493,10 @@ var ThunderBridge = class {
2981
3493
  * the recipient's own wallet minted it, so a payer who pays the loser afterwards
2982
3494
  * really does pay twice and that shows up on `follow` as a second settlement to
2983
3495
  * refund.
3496
+ *
3497
+ * A leg the gateway is caught lying about before any leg is paid ends the wait
3498
+ * with its `GatewayCheatError`, even when another leg is paid after it, so a
3499
+ * detected cheat is never traded for a later win.
2984
3500
  */
2985
3501
  async firstSettled(held, options) {
2986
3502
  if (held.length === 0) {
@@ -2990,7 +3506,7 @@ var ThunderBridge = class {
2990
3506
  const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
2991
3507
  let refused = null;
2992
3508
  try {
2993
- const winner = await new Promise((resolve2) => {
3509
+ const winner = await new Promise((resolve2, caught) => {
2994
3510
  let waiting = held.length;
2995
3511
  const lost = () => {
2996
3512
  waiting -= 1;
@@ -3000,6 +3516,10 @@ var ThunderBridge = class {
3000
3516
  };
3001
3517
  for (const leg of held) {
3002
3518
  this.settled(leg, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
3519
+ if (failure instanceof GatewayCheatError) {
3520
+ caught(failure);
3521
+ return;
3522
+ }
3003
3523
  refused ??= failure;
3004
3524
  lost();
3005
3525
  });
@@ -3076,13 +3596,13 @@ var ThunderBridge = class {
3076
3596
  }
3077
3597
  retry = setTimeout(() => void connect(), backoffMs(firstDelay, attempt));
3078
3598
  };
3079
- const answered3 = async (opened, raw) => {
3599
+ const answered3 = async (opened2, raw) => {
3080
3600
  const asked = askFromWire(raw);
3081
3601
  if (asked === null) {
3082
3602
  return;
3083
3603
  }
3084
3604
  const preimage = await options.answer(asked.paymentHash);
3085
- opened.send(
3605
+ opened2.send(
3086
3606
  JSON.stringify(
3087
3607
  preimage === null ? { ask: asked.ask, settled: false } : { ask: asked.ask, settled: true, preimage }
3088
3608
  )
@@ -3101,18 +3621,18 @@ var ThunderBridge = class {
3101
3621
  if (stopped) {
3102
3622
  return;
3103
3623
  }
3104
- const opened = new WebSocket(url);
3105
- socket = opened;
3106
- opened.onopen = () => {
3624
+ const opened2 = new WebSocket(url);
3625
+ socket = opened2;
3626
+ opened2.onopen = () => {
3107
3627
  attempt = 1;
3108
3628
  };
3109
- opened.onmessage = (event) => {
3110
- void answered3(opened, String(event.data)).catch(
3629
+ opened2.onmessage = (event) => {
3630
+ void answered3(opened2, String(event.data)).catch(
3111
3631
  (refused) => options.onError?.(refused)
3112
3632
  );
3113
3633
  };
3114
- opened.onerror = () => options.onError?.(new Error(`could not attend ${base}`));
3115
- opened.onclose = again;
3634
+ opened2.onerror = () => options.onError?.(new Error(`could not attend ${base}`));
3635
+ opened2.onclose = again;
3116
3636
  };
3117
3637
  void connect();
3118
3638
  return () => {
@@ -3227,7 +3747,7 @@ var ThunderBridge = class {
3227
3747
  async reading(method, path, body = "") {
3228
3748
  return {
3229
3749
  ...this.token === null ? {} : { authorization: `Bearer ${this.token}` },
3230
- ...this.secret === null ? {} : await signedAs(await this.speaking(), method, path, body)
3750
+ ...this.secret === null ? {} : await signedAs(await this.speaking(), method, path, body, new URL(this.baseUrl).host)
3231
3751
  };
3232
3752
  }
3233
3753
  async speaking() {
@@ -3253,7 +3773,7 @@ var ThunderBridge = class {
3253
3773
  if (other) {
3254
3774
  throw new GatewayCheatError("hash_mismatch", payment.id);
3255
3775
  }
3256
- if (payment.status === "paid" && !carriesProof(payment)) {
3776
+ if (payment.status === "paid" && !agreesWithItself(payment)) {
3257
3777
  throw new GatewayCheatError("preimage_mismatch", payment.id);
3258
3778
  }
3259
3779
  return payment;
@@ -3307,6 +3827,7 @@ var Gateways = class {
3307
3827
  each;
3308
3828
  urls;
3309
3829
  onRefused;
3830
+ onCaught;
3310
3831
  constructor(baseUrls, options) {
3311
3832
  if (baseUrls.length === 0) {
3312
3833
  throw new Error("name at least one gateway to watch at");
@@ -3315,6 +3836,8 @@ var Gateways = class {
3315
3836
  this.each = baseUrls.map((baseUrl) => new ThunderBridge(baseUrl, options));
3316
3837
  this.onRefused = options?.onRefused ?? (() => {
3317
3838
  });
3839
+ this.onCaught = options?.onCaught ?? (() => {
3840
+ });
3318
3841
  }
3319
3842
  /** What this payment is called, which every gateway here will agree on */
3320
3843
  async nameFor(paymentHash) {
@@ -3362,7 +3885,7 @@ var Gateways = class {
3362
3885
  resolve2(null);
3363
3886
  }
3364
3887
  };
3365
- for (const gateway of this.each) {
3888
+ for (const [at, gateway] of this.each.entries()) {
3366
3889
  gateway.settled(held, { ...options, signal }).then((watched) => {
3367
3890
  if (watched.status === "paid") {
3368
3891
  return resolve2(watched);
@@ -3370,6 +3893,9 @@ var Gateways = class {
3370
3893
  unpaid.push(watched);
3371
3894
  lost();
3372
3895
  }).catch((failure) => {
3896
+ if (failure instanceof GatewayCheatError) {
3897
+ this.onCaught(this.urls[at], failure);
3898
+ }
3373
3899
  refused ??= failure;
3374
3900
  lost();
3375
3901
  });
@@ -3398,6 +3924,7 @@ var Gateways = class {
3398
3924
  ThunderBridge,
3399
3925
  UnverifiedRecipientError,
3400
3926
  WrapRefusedError,
3927
+ agreesWithItself,
3401
3928
  answerVerifyChallenge,
3402
3929
  carriesProof,
3403
3930
  decodeInvoice,