thunder-bridge 1.5.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/{client-CzGZcByI.d.cts → bank-CEIuNhV_.d.cts} +191 -28
- package/dist/{client-Cc5gtjGV.d.ts → bank-M8flcMks.d.ts} +191 -28
- package/dist/bank.cjs +197 -0
- package/dist/bank.d.cts +4 -2
- package/dist/bank.d.ts +4 -2
- package/dist/bank.js +196 -0
- package/dist/{errors-Dmh-Uoh8.d.cts → errors-DriqW-lp.d.cts} +2 -2
- package/dist/{errors-0vbVoISA.d.ts → errors-ZEUT-MhO.d.ts} +2 -2
- package/dist/index.cjs +701 -310
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +701 -310
- package/dist/nwc.cjs +115 -31
- package/dist/nwc.d.cts +6 -4
- package/dist/nwc.d.ts +6 -4
- package/dist/nwc.js +112 -28
- package/dist/price.d.cts +2 -2
- package/dist/price.d.ts +2 -2
- package/dist/qr.cjs +20 -23
- package/dist/qr.js +20 -23
- package/dist/{types-BNPmVnA7.d.cts → types-DYZ9EkmJ.d.cts} +11 -2
- package/dist/{types-BNPmVnA7.d.ts → types-DYZ9EkmJ.d.ts} +11 -2
- package/openapi.yaml +29 -39
- package/package.json +1 -1
- package/dist/bank-B3mHISn1.d.cts +0 -92
- package/dist/bank-B3mHISn1.d.ts +0 -92
package/dist/index.js
CHANGED
|
@@ -175,6 +175,7 @@ function rotate(value, by) {
|
|
|
175
175
|
|
|
176
176
|
// ../core/bolt11.ts
|
|
177
177
|
var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
178
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
178
179
|
var CHECKSUM_WORDS = 6;
|
|
179
180
|
var TIMESTAMP_WORDS = 7;
|
|
180
181
|
var SIGNATURE_WORDS = 104;
|
|
@@ -192,18 +193,27 @@ var HRP_MULTIPLIER_MSAT = {
|
|
|
192
193
|
n: MSAT_PER_BTC / 1e9,
|
|
193
194
|
p: MSAT_PER_BTC / 1e12
|
|
194
195
|
};
|
|
195
|
-
var
|
|
196
|
+
var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
|
|
196
197
|
function decodeInvoice(bolt11) {
|
|
197
198
|
const parts = splitBech32(bolt11);
|
|
198
199
|
if (parts === null) {
|
|
199
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
paymentHash: null,
|
|
202
|
+
descriptionHash: null,
|
|
203
|
+
amountMsat: null,
|
|
204
|
+
issuedAt: null,
|
|
205
|
+
expiresAt: null
|
|
206
|
+
};
|
|
200
207
|
}
|
|
201
208
|
const tagged = taggedFields(parts.words);
|
|
209
|
+
const issuedAt = issuedAtOf(parts.words);
|
|
210
|
+
const expiry = tagged.get(EXPIRY_TAG);
|
|
202
211
|
return {
|
|
203
212
|
paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
|
|
204
213
|
descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
|
|
205
214
|
amountMsat: amountFromHrp(parts.hrp),
|
|
206
|
-
|
|
215
|
+
issuedAt,
|
|
216
|
+
expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
|
|
207
217
|
};
|
|
208
218
|
}
|
|
209
219
|
function preimageMatchesHash(preimage, paymentHash) {
|
|
@@ -214,7 +224,7 @@ function preimageMatchesHash(preimage, paymentHash) {
|
|
|
214
224
|
}
|
|
215
225
|
function splitBech32(bolt11) {
|
|
216
226
|
const lower = bolt11.toLowerCase();
|
|
217
|
-
if (isBolt12Encoding(lower)) {
|
|
227
|
+
if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
|
|
218
228
|
return null;
|
|
219
229
|
}
|
|
220
230
|
const separator = lower.lastIndexOf("1");
|
|
@@ -233,7 +243,28 @@ function splitBech32(bolt11) {
|
|
|
233
243
|
}
|
|
234
244
|
words.push(value);
|
|
235
245
|
}
|
|
236
|
-
|
|
246
|
+
const hrp = lower.slice(0, separator);
|
|
247
|
+
if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
|
|
251
|
+
}
|
|
252
|
+
function expandedHrp(hrp) {
|
|
253
|
+
const codes = [...hrp].map((char) => char.charCodeAt(0));
|
|
254
|
+
return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
|
|
255
|
+
}
|
|
256
|
+
function bech32Polymod(values) {
|
|
257
|
+
let checksum = 1;
|
|
258
|
+
for (const value of values) {
|
|
259
|
+
const top = checksum >> 25;
|
|
260
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
261
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
262
|
+
if (top >> bit & 1) {
|
|
263
|
+
checksum ^= BECH32_GENERATOR[bit];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return checksum;
|
|
237
268
|
}
|
|
238
269
|
function isBolt12Encoding(lower) {
|
|
239
270
|
return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
|
|
@@ -263,19 +294,17 @@ function hexTag(fields, tag, expectedWords) {
|
|
|
263
294
|
}
|
|
264
295
|
return bytesToHex(wordsToBytes(words));
|
|
265
296
|
}
|
|
266
|
-
function
|
|
297
|
+
function issuedAtOf(words) {
|
|
267
298
|
if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
|
|
268
299
|
return null;
|
|
269
300
|
}
|
|
270
|
-
|
|
271
|
-
return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
|
|
301
|
+
return readNumber(words.slice(0, TIMESTAMP_WORDS));
|
|
272
302
|
}
|
|
273
303
|
function amountFromHrp(hrp) {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
304
|
+
const [, digits, multiplier] = HRP.exec(hrp) ?? [];
|
|
305
|
+
if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
|
|
276
306
|
return null;
|
|
277
307
|
}
|
|
278
|
-
const [, digits, multiplier] = match;
|
|
279
308
|
return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
|
|
280
309
|
}
|
|
281
310
|
function readNumber(words) {
|
|
@@ -296,28 +325,74 @@ function wordsToBytes(words) {
|
|
|
296
325
|
return Uint8Array.from(bytes);
|
|
297
326
|
}
|
|
298
327
|
|
|
328
|
+
// ../core/hmac.ts
|
|
329
|
+
async function hmacHex(secret, payload) {
|
|
330
|
+
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
331
|
+
const key = await crypto.subtle.importKey(
|
|
332
|
+
"raw",
|
|
333
|
+
asBuffer(raw),
|
|
334
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
335
|
+
false,
|
|
336
|
+
["sign"]
|
|
337
|
+
);
|
|
338
|
+
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
339
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
|
|
340
|
+
}
|
|
341
|
+
function equalInConstantTime(one, other) {
|
|
342
|
+
if (one.length !== other.length) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
let difference = 0;
|
|
346
|
+
for (let at = 0; at < one.length; at++) {
|
|
347
|
+
difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
348
|
+
}
|
|
349
|
+
return difference === 0;
|
|
350
|
+
}
|
|
351
|
+
function asBuffer(bytes) {
|
|
352
|
+
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
353
|
+
}
|
|
354
|
+
|
|
299
355
|
// ../core/sealed.ts
|
|
300
|
-
var VERSION = "
|
|
356
|
+
var VERSION = "v2";
|
|
357
|
+
var UNPADDED_VERSION = "v1";
|
|
301
358
|
var IV_BYTES = 12;
|
|
302
359
|
var MIN_SECRET_CHARS = 32;
|
|
303
360
|
var MAX_PLAIN_BYTES = 3e3;
|
|
361
|
+
var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
|
|
362
|
+
var END_OF_TEXT = 128;
|
|
304
363
|
var INFO = new TextEncoder().encode("thunder-bridge/sealed");
|
|
305
|
-
async function seal(secret, plaintext) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
364
|
+
async function seal(secret, plaintext, paymentHash) {
|
|
365
|
+
return await sealUnder(
|
|
366
|
+
secret,
|
|
367
|
+
plaintext,
|
|
368
|
+
crypto.getRandomValues(new Uint8Array(IV_BYTES)),
|
|
369
|
+
boundTo(paymentHash)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
async function sealStable(secret, plaintext) {
|
|
373
|
+
const derived = await hmacHex(secret, `sealed-nonce|${plaintext}`);
|
|
374
|
+
const nonce = new Uint8Array(IV_BYTES);
|
|
375
|
+
for (let at = 0; at < IV_BYTES; at++) {
|
|
376
|
+
nonce[at] = Number.parseInt(derived.slice(at * 2, at * 2 + 2), 16);
|
|
309
377
|
}
|
|
378
|
+
return await sealUnder(secret, plaintext, nonce, boundTo(void 0));
|
|
379
|
+
}
|
|
380
|
+
async function sealUnder(secret, plaintext, iv, additionalData) {
|
|
381
|
+
const body = padToClass(plaintext);
|
|
310
382
|
const key = await keyFor(secret);
|
|
311
|
-
const
|
|
312
|
-
|
|
383
|
+
const cipher = new Uint8Array(
|
|
384
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
|
|
385
|
+
);
|
|
313
386
|
const joined = new Uint8Array(iv.length + cipher.length);
|
|
314
387
|
joined.set(iv);
|
|
315
388
|
joined.set(cipher, iv.length);
|
|
316
389
|
return `${VERSION}.${toBase64Url(joined)}`;
|
|
317
390
|
}
|
|
318
|
-
async function unseal(secret, sealed) {
|
|
391
|
+
async function unseal(secret, sealed, paymentHash) {
|
|
319
392
|
const key = await keyFor(secret);
|
|
320
|
-
|
|
393
|
+
const padded = sealed.startsWith(`${VERSION}.`);
|
|
394
|
+
const unbound = !padded && sealed.startsWith(`${UNPADDED_VERSION}.`);
|
|
395
|
+
if (!padded && (!unbound || paymentHash !== void 0)) {
|
|
321
396
|
return null;
|
|
322
397
|
}
|
|
323
398
|
const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
|
|
@@ -325,20 +400,50 @@ async function unseal(secret, sealed) {
|
|
|
325
400
|
return null;
|
|
326
401
|
}
|
|
327
402
|
try {
|
|
328
|
-
const body =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
403
|
+
const body = new Uint8Array(
|
|
404
|
+
await crypto.subtle.decrypt(
|
|
405
|
+
{
|
|
406
|
+
name: "AES-GCM",
|
|
407
|
+
iv: joined.slice(0, IV_BYTES),
|
|
408
|
+
additionalData: padded ? boundTo(paymentHash) : void 0
|
|
409
|
+
},
|
|
410
|
+
key,
|
|
411
|
+
joined.slice(IV_BYTES)
|
|
412
|
+
)
|
|
332
413
|
);
|
|
333
|
-
|
|
414
|
+
const text3 = padded ? unpad(body) : body;
|
|
415
|
+
return text3 === null ? null : new TextDecoder().decode(text3);
|
|
334
416
|
} catch {
|
|
335
417
|
return null;
|
|
336
418
|
}
|
|
337
419
|
}
|
|
338
|
-
|
|
420
|
+
function refuseAWeakSecret(secret) {
|
|
339
421
|
if (secret.length < MIN_SECRET_CHARS) {
|
|
340
422
|
throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
|
|
341
423
|
}
|
|
424
|
+
}
|
|
425
|
+
function boundTo(paymentHash) {
|
|
426
|
+
return new TextEncoder().encode(
|
|
427
|
+
paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
function padToClass(plaintext) {
|
|
431
|
+
const text3 = new TextEncoder().encode(plaintext);
|
|
432
|
+
const size = SIZE_CLASSES.find((limit) => text3.length <= limit);
|
|
433
|
+
if (size === void 0) {
|
|
434
|
+
throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text3.length}`);
|
|
435
|
+
}
|
|
436
|
+
const body = new Uint8Array(size + 1);
|
|
437
|
+
body.set(text3);
|
|
438
|
+
body[text3.length] = END_OF_TEXT;
|
|
439
|
+
return body;
|
|
440
|
+
}
|
|
441
|
+
function unpad(body) {
|
|
442
|
+
const end = body.lastIndexOf(END_OF_TEXT);
|
|
443
|
+
return end === -1 ? null : body.subarray(0, end);
|
|
444
|
+
}
|
|
445
|
+
async function keyFor(secret) {
|
|
446
|
+
refuseAWeakSecret(secret);
|
|
342
447
|
const material = await crypto.subtle.importKey(
|
|
343
448
|
"raw",
|
|
344
449
|
new TextEncoder().encode(secret),
|
|
@@ -535,11 +640,11 @@ function coinbase(baseUrl = "https://api.coinbase.com") {
|
|
|
535
640
|
return async (currency) => {
|
|
536
641
|
const asked = currency.toUpperCase();
|
|
537
642
|
const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
|
|
538
|
-
const
|
|
539
|
-
if (
|
|
540
|
-
throw new Error(`coinbase quoted ${
|
|
643
|
+
const quoted2 = answer.data;
|
|
644
|
+
if (quoted2?.currency !== asked) {
|
|
645
|
+
throw new Error(`coinbase quoted ${quoted2?.currency} when asked for ${asked}`);
|
|
541
646
|
}
|
|
542
|
-
return asMinor(
|
|
647
|
+
return asMinor(quoted2.amount, "coinbase", asked);
|
|
543
648
|
};
|
|
544
649
|
}
|
|
545
650
|
function kraken(baseUrl = "https://api.kraken.com") {
|
|
@@ -588,19 +693,19 @@ function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], opti
|
|
|
588
693
|
return remembered.price;
|
|
589
694
|
}
|
|
590
695
|
const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
|
|
591
|
-
const
|
|
592
|
-
if (
|
|
696
|
+
const quoted2 = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
|
|
697
|
+
if (quoted2.length < minVenues) {
|
|
593
698
|
throw new Error(
|
|
594
|
-
`${
|
|
699
|
+
`${quoted2.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
|
|
595
700
|
);
|
|
596
701
|
}
|
|
597
|
-
const spread2 = spreadBpsOf(
|
|
702
|
+
const spread2 = spreadBpsOf(quoted2);
|
|
598
703
|
if (spread2 > maxSpread) {
|
|
599
704
|
throw new Error(
|
|
600
|
-
`venues disagree on ${asked} by ${Math.round(spread2)} bps, more than the ${maxSpread} allowed: ${
|
|
705
|
+
`venues disagree on ${asked} by ${Math.round(spread2)} bps, more than the ${maxSpread} allowed: ${quoted2.join(", ")}`
|
|
601
706
|
);
|
|
602
707
|
}
|
|
603
|
-
const price = middleOf(
|
|
708
|
+
const price = middleOf(quoted2);
|
|
604
709
|
held.set(asked, { at: Date.now(), price });
|
|
605
710
|
return price;
|
|
606
711
|
};
|
|
@@ -631,10 +736,10 @@ async function asJson(url, venue) {
|
|
|
631
736
|
}
|
|
632
737
|
return answer.json();
|
|
633
738
|
}
|
|
634
|
-
function asMinor(
|
|
635
|
-
const major2 = Number(
|
|
739
|
+
function asMinor(quoted2, venue, currency) {
|
|
740
|
+
const major2 = Number(quoted2);
|
|
636
741
|
if (!Number.isFinite(major2) || major2 <= 0) {
|
|
637
|
-
throw new Error(`${venue} quoted ${String(
|
|
742
|
+
throw new Error(`${venue} quoted ${String(quoted2)} for ${currency.toUpperCase()}`);
|
|
638
743
|
}
|
|
639
744
|
return Math.round(major2 * minorScaleOf(currency));
|
|
640
745
|
}
|
|
@@ -722,6 +827,16 @@ var SEED_BYTES = 32;
|
|
|
722
827
|
var SIGNATURE_BYTES = 64;
|
|
723
828
|
var PUBLIC_KEY_BYTES = 32;
|
|
724
829
|
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
830
|
+
var SIGN_BIT_CLEARED = 127;
|
|
831
|
+
var SMALL_ORDER = [
|
|
832
|
+
"00".repeat(32),
|
|
833
|
+
`01${"00".repeat(31)}`,
|
|
834
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
835
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
836
|
+
`ec${"ff".repeat(30)}7f`,
|
|
837
|
+
`ed${"ff".repeat(30)}7f`,
|
|
838
|
+
`ee${"ff".repeat(30)}7f`
|
|
839
|
+
];
|
|
725
840
|
async function signingKeyFromSeed(seed) {
|
|
726
841
|
if (seed.length !== SEED_BYTES) {
|
|
727
842
|
throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
|
|
@@ -729,24 +844,27 @@ async function signingKeyFromSeed(seed) {
|
|
|
729
844
|
const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
|
|
730
845
|
wrapped.set(PKCS8_HEADER);
|
|
731
846
|
wrapped.set(seed, PKCS8_HEADER.length);
|
|
732
|
-
const key = await crypto.subtle.importKey("pkcs8",
|
|
847
|
+
const key = await crypto.subtle.importKey("pkcs8", asBuffer2(wrapped), ALGORITHM, true, ["sign"]);
|
|
733
848
|
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
734
849
|
return {
|
|
735
850
|
publicKeyHex: bytesToHex(fromBase64Url2(jwk.x ?? "")),
|
|
736
|
-
sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key,
|
|
851
|
+
sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer2(payload))))
|
|
737
852
|
};
|
|
738
853
|
}
|
|
739
854
|
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
740
855
|
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) {
|
|
741
856
|
return false;
|
|
742
857
|
}
|
|
858
|
+
if (hasSmallOrder(publicKeyHex)) {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
743
861
|
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) {
|
|
744
862
|
return false;
|
|
745
863
|
}
|
|
746
864
|
try {
|
|
747
865
|
const key = await crypto.subtle.importKey(
|
|
748
866
|
"raw",
|
|
749
|
-
|
|
867
|
+
asBuffer2(hexToBytes(publicKeyHex)),
|
|
750
868
|
ALGORITHM,
|
|
751
869
|
false,
|
|
752
870
|
["verify"]
|
|
@@ -754,13 +872,18 @@ async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
|
754
872
|
return await crypto.subtle.verify(
|
|
755
873
|
ALGORITHM,
|
|
756
874
|
key,
|
|
757
|
-
|
|
758
|
-
|
|
875
|
+
asBuffer2(hexToBytes(signatureHex)),
|
|
876
|
+
asBuffer2(payload)
|
|
759
877
|
);
|
|
760
878
|
} catch {
|
|
761
879
|
return false;
|
|
762
880
|
}
|
|
763
881
|
}
|
|
882
|
+
function hasSmallOrder(publicKeyHex) {
|
|
883
|
+
const point = hexToBytes(publicKeyHex);
|
|
884
|
+
point[PUBLIC_KEY_BYTES - 1] = point[PUBLIC_KEY_BYTES - 1] & SIGN_BIT_CLEARED;
|
|
885
|
+
return SMALL_ORDER.includes(bytesToHex(point));
|
|
886
|
+
}
|
|
764
887
|
function fromBase64Url2(text3) {
|
|
765
888
|
const padded = text3.replace(/-/g, "+").replace(/_/g, "/");
|
|
766
889
|
const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
@@ -770,35 +893,8 @@ function fromBase64Url2(text3) {
|
|
|
770
893
|
}
|
|
771
894
|
return bytes;
|
|
772
895
|
}
|
|
773
|
-
function asBuffer(bytes) {
|
|
774
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// ../core/hmac.ts
|
|
778
|
-
async function hmacHex(secret, payload) {
|
|
779
|
-
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
780
|
-
const key = await crypto.subtle.importKey(
|
|
781
|
-
"raw",
|
|
782
|
-
asBuffer2(raw),
|
|
783
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
784
|
-
false,
|
|
785
|
-
["sign"]
|
|
786
|
-
);
|
|
787
|
-
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
788
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
|
|
789
|
-
}
|
|
790
|
-
function equalInConstantTime(one, other) {
|
|
791
|
-
if (one.length !== other.length) {
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
let difference = 0;
|
|
795
|
-
for (let at = 0; at < one.length; at++) {
|
|
796
|
-
difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
797
|
-
}
|
|
798
|
-
return difference === 0;
|
|
799
|
-
}
|
|
800
896
|
function asBuffer2(bytes) {
|
|
801
|
-
return bytes
|
|
897
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
802
898
|
}
|
|
803
899
|
|
|
804
900
|
// ../core/caller.ts
|
|
@@ -876,8 +972,8 @@ function ipv4IsGlobal(literal) {
|
|
|
876
972
|
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
877
973
|
return false;
|
|
878
974
|
}
|
|
879
|
-
const [a, b] = parts;
|
|
880
|
-
if (a === 0 || a === 10 || a === 127 || a >=
|
|
975
|
+
const [a, b, c] = parts;
|
|
976
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224) {
|
|
881
977
|
return false;
|
|
882
978
|
}
|
|
883
979
|
if (a === 100 && b >= 64 && b < 128) {
|
|
@@ -889,7 +985,13 @@ function ipv4IsGlobal(literal) {
|
|
|
889
985
|
if (a === 172 && b >= 16 && b < 32) {
|
|
890
986
|
return false;
|
|
891
987
|
}
|
|
892
|
-
if (a === 192 && b === 168) {
|
|
988
|
+
if (a === 192 && (b === 168 || b === 0 && (c === 0 || c === 2))) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
if (a === 198 && (b === 18 || b === 19 || b === 51 && c === 100)) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (a === 203 && b === 0 && c === 113) {
|
|
893
995
|
return false;
|
|
894
996
|
}
|
|
895
997
|
return true;
|
|
@@ -899,29 +1001,59 @@ function ipv6IsGlobal(literal) {
|
|
|
899
1001
|
if (groups.length !== 8 || groups.some(Number.isNaN)) {
|
|
900
1002
|
return false;
|
|
901
1003
|
}
|
|
902
|
-
const
|
|
903
|
-
|
|
1004
|
+
const [first, second, third, fourth, fifth, sixth, high, low] = groups;
|
|
1005
|
+
const embedded = (one, other) => ipv4IsGlobal(`${one >> 8}.${one & 255}.${other >> 8}.${other & 255}`);
|
|
1006
|
+
const zeroBefore = (count) => groups.slice(0, count).every((group) => group === 0);
|
|
1007
|
+
if (zeroBefore(5) && sixth === 65535) {
|
|
1008
|
+
return embedded(high, low);
|
|
1009
|
+
}
|
|
1010
|
+
if (zeroBefore(6) || zeroBefore(4) && fifth === 65535 && sixth === 0) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
if (first === 100 && second === 65435) {
|
|
1014
|
+
return third === 0 && fourth === 0 && fifth === 0 && sixth === 0 && embedded(high, low);
|
|
1015
|
+
}
|
|
1016
|
+
if (first === 8194) {
|
|
1017
|
+
return embedded(second, third);
|
|
1018
|
+
}
|
|
1019
|
+
if (first === 8193 && second === 0) {
|
|
904
1020
|
return false;
|
|
905
1021
|
}
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
1022
|
+
if (first === 256 && second === 0 && third === 0 && fourth === 0) {
|
|
1023
|
+
return false;
|
|
909
1024
|
}
|
|
910
|
-
|
|
911
|
-
if ((first & 65024) === 64512) {
|
|
1025
|
+
if ((first & 65024) === 64512 || (first & 65280) === 65280) {
|
|
912
1026
|
return false;
|
|
913
1027
|
}
|
|
914
|
-
if ((first & 65472) === 65152) {
|
|
1028
|
+
if ((first & 65472) === 65152 || (first & 65472) === 65216) {
|
|
915
1029
|
return false;
|
|
916
1030
|
}
|
|
917
1031
|
return true;
|
|
918
1032
|
}
|
|
919
1033
|
function ipv6Groups(address) {
|
|
920
|
-
const
|
|
1034
|
+
const halves = withDottedTailAsGroups(address).split("::");
|
|
1035
|
+
const [head = "", tail] = halves;
|
|
921
1036
|
const left = head.split(":").filter((group) => group !== "");
|
|
922
1037
|
const right = (tail ?? "").split(":").filter((group) => group !== "");
|
|
923
|
-
const
|
|
924
|
-
|
|
1038
|
+
const missing = 8 - left.length - right.length;
|
|
1039
|
+
if (halves.length > 2 || missing < 0) {
|
|
1040
|
+
return [Number.NaN];
|
|
1041
|
+
}
|
|
1042
|
+
const gap = tail === void 0 ? [] : Array(missing).fill("0");
|
|
1043
|
+
return [...left, ...gap, ...right].map(
|
|
1044
|
+
(group) => /^[0-9a-f]{1,4}$/.test(group) ? parseInt(group, 16) : Number.NaN
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
function withDottedTailAsGroups(address) {
|
|
1048
|
+
const dotted = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(address);
|
|
1049
|
+
if (dotted === null) {
|
|
1050
|
+
return address;
|
|
1051
|
+
}
|
|
1052
|
+
const [a, b, c, d] = dotted[2].split(".").map(Number);
|
|
1053
|
+
if ([a, b, c, d].some((part) => part > 255)) {
|
|
1054
|
+
return `${dotted[1]}x`;
|
|
1055
|
+
}
|
|
1056
|
+
return `${dotted[1]}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
925
1057
|
}
|
|
926
1058
|
|
|
927
1059
|
// ../core/outbound.ts
|
|
@@ -949,6 +1081,9 @@ async function ask(send, url, sent = {}) {
|
|
|
949
1081
|
}
|
|
950
1082
|
await response.body?.cancel();
|
|
951
1083
|
const next = new URL(location, target).toString();
|
|
1084
|
+
if (sent.staysOnOrigin && !sameOrigin(target, next)) {
|
|
1085
|
+
throw new Error(`${target} redirected off its own origin, to ${next}`);
|
|
1086
|
+
}
|
|
952
1087
|
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
953
1088
|
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
954
1089
|
target = next;
|
|
@@ -995,15 +1130,17 @@ async function answerOf(response) {
|
|
|
995
1130
|
return { status, ok, body: text(read), truncated: true, headers };
|
|
996
1131
|
}
|
|
997
1132
|
function withoutBody(sent) {
|
|
998
|
-
const headers =
|
|
999
|
-
|
|
1133
|
+
const headers = Object.fromEntries(
|
|
1134
|
+
Object.entries(sent.headers ?? {}).filter(([name]) => name.toLowerCase() !== "content-type")
|
|
1135
|
+
);
|
|
1000
1136
|
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1001
1137
|
}
|
|
1002
1138
|
function withoutCredentials(sent) {
|
|
1003
|
-
const headers =
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1139
|
+
const headers = Object.fromEntries(
|
|
1140
|
+
Object.entries(sent.headers ?? {}).filter(
|
|
1141
|
+
([name]) => !CREDENTIALS.includes(name.toLowerCase())
|
|
1142
|
+
)
|
|
1143
|
+
);
|
|
1007
1144
|
return { ...sent, headers };
|
|
1008
1145
|
}
|
|
1009
1146
|
function text(chunks) {
|
|
@@ -1046,6 +1183,22 @@ async function resolve(send, addresses, amountMsat) {
|
|
|
1046
1183
|
);
|
|
1047
1184
|
return served.won;
|
|
1048
1185
|
}
|
|
1186
|
+
async function quote(send, addresses, amountMsat) {
|
|
1187
|
+
const served = await firstThatServes(
|
|
1188
|
+
addresses,
|
|
1189
|
+
(address2, deadline) => probe(send, address2, amountMsat, deadline)
|
|
1190
|
+
);
|
|
1191
|
+
const { address, pay } = served.won;
|
|
1192
|
+
return {
|
|
1193
|
+
won: {
|
|
1194
|
+
address,
|
|
1195
|
+
minMsat: pay.minSendable,
|
|
1196
|
+
maxMsat: pay.maxSendable,
|
|
1197
|
+
metadata: pay.metadata
|
|
1198
|
+
},
|
|
1199
|
+
refusals: served.refusals
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1049
1202
|
async function firstThatServes(addresses, attempt) {
|
|
1050
1203
|
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1051
1204
|
const refusals3 = [];
|
|
@@ -1138,7 +1291,7 @@ function decodeIssued(address, bolt11) {
|
|
|
1138
1291
|
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1139
1292
|
}
|
|
1140
1293
|
async function checkSettled(send, verifyUrl, paymentHash) {
|
|
1141
|
-
const answer = await answeredJson(send, verifyUrl);
|
|
1294
|
+
const answer = await answeredJson(send, verifyUrl, { staysOnOrigin: true });
|
|
1142
1295
|
const asked = {
|
|
1143
1296
|
pace: paceAskedFor(answer.headers),
|
|
1144
1297
|
ceiling: ceilingAskedFor(answer.headers)
|
|
@@ -1196,10 +1349,10 @@ function isNothingButAHost(domain) {
|
|
|
1196
1349
|
}
|
|
1197
1350
|
}
|
|
1198
1351
|
async function fetchJson(send, url, deadline) {
|
|
1199
|
-
return (await answeredJson(send, url, deadline)).said;
|
|
1352
|
+
return (await answeredJson(send, url, { deadline })).said;
|
|
1200
1353
|
}
|
|
1201
|
-
async function answeredJson(send, url,
|
|
1202
|
-
const answer = await ask(send, url, { headers: { accept: "application/json" },
|
|
1354
|
+
async function answeredJson(send, url, sent) {
|
|
1355
|
+
const answer = await ask(send, url, { headers: { accept: "application/json" }, ...sent });
|
|
1203
1356
|
if (!answer.ok) {
|
|
1204
1357
|
throw new Error(`${url} answered ${answer.status}`);
|
|
1205
1358
|
}
|
|
@@ -1227,12 +1380,17 @@ var pinnedToTheAddressWeVerified = async (url, sent, signal, at) => {
|
|
|
1227
1380
|
}
|
|
1228
1381
|
const call = request(reaching(asked, sent, at, first), (answer) => {
|
|
1229
1382
|
const status = answer.statusCode ?? 502;
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1383
|
+
try {
|
|
1384
|
+
settle(
|
|
1385
|
+
new Response(
|
|
1386
|
+
CARRIES_NO_BODY.includes(status) ? null : Readable.toWeb(answer),
|
|
1387
|
+
{ status, headers: headersOf(answer.headers) }
|
|
1388
|
+
)
|
|
1389
|
+
);
|
|
1390
|
+
} catch (unreadable) {
|
|
1391
|
+
answer.destroy();
|
|
1392
|
+
fail(new Error(`${url} answered with something HTTP cannot carry`, { cause: unreadable }));
|
|
1393
|
+
}
|
|
1236
1394
|
});
|
|
1237
1395
|
const give = () => call.destroy(new Error(`${url} took longer than allowed`));
|
|
1238
1396
|
signal.addEventListener("abort", give, { once: true });
|
|
@@ -1279,6 +1437,173 @@ function headersOf(raw) {
|
|
|
1279
1437
|
return headers;
|
|
1280
1438
|
}
|
|
1281
1439
|
|
|
1440
|
+
// src/verify.ts
|
|
1441
|
+
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1442
|
+
var ISSUED_SKEW_SECS = 300;
|
|
1443
|
+
var NAMES_AN_ADDRESS = ["text/identifier", "text/email"];
|
|
1444
|
+
var DEFAULT_WRAP_PROPORTION = 0.01;
|
|
1445
|
+
var DEFAULT_WRAP_BASE_MSAT = 1e3;
|
|
1446
|
+
async function proveOrigin(payment, asked, askedAt) {
|
|
1447
|
+
const cheat = (code) => new GatewayCheatError(code, payment.id);
|
|
1448
|
+
const listed2 = asked.paidTo.find((address) => equalIgnoringCase(address, payment.lnAddress));
|
|
1449
|
+
if (listed2 === void 0) {
|
|
1450
|
+
throw cheat("address_not_requested");
|
|
1451
|
+
}
|
|
1452
|
+
if (payment.amountMsat !== asked.amountMsat) {
|
|
1453
|
+
throw cheat("amount_mismatch");
|
|
1454
|
+
}
|
|
1455
|
+
const invoice = decodeInvoice(payment.bolt11);
|
|
1456
|
+
if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
|
|
1457
|
+
throw cheat("hash_mismatch");
|
|
1458
|
+
}
|
|
1459
|
+
if (invoice.amountMsat !== asked.amountMsat) {
|
|
1460
|
+
throw cheat("amount_mismatch");
|
|
1461
|
+
}
|
|
1462
|
+
if (askedAt !== void 0 && (invoice.issuedAt ?? 0) < askedAt - ISSUED_SKEW_SECS) {
|
|
1463
|
+
throw cheat("invoice_stale");
|
|
1464
|
+
}
|
|
1465
|
+
const payRequest = await payRequestFor(listed2, payment);
|
|
1466
|
+
if (typeof payRequest.metadata !== "string" || typeof payRequest.callback !== "string") {
|
|
1467
|
+
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "no payRequest served");
|
|
1468
|
+
}
|
|
1469
|
+
if (!namesTheAddress(payRequest.metadata, listed2)) {
|
|
1470
|
+
throw new UnverifiedRecipientError(
|
|
1471
|
+
payment.lnAddress,
|
|
1472
|
+
payment.id,
|
|
1473
|
+
"its metadata names no text/identifier for this address, so no invoice binds to it"
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
if (invoice.descriptionHash !== sha256Hex(payRequest.metadata)) {
|
|
1477
|
+
throw cheat("description_hash_mismatch");
|
|
1478
|
+
}
|
|
1479
|
+
if (!sameOrigin(payment.verifyUrl, payRequest.callback)) {
|
|
1480
|
+
throw cheat("verify_url_foreign");
|
|
1481
|
+
}
|
|
1482
|
+
const issued = await reachable(payment.verifyUrl, payment);
|
|
1483
|
+
if (typeof issued.pr !== "string" || !equalIgnoringCase(issued.pr, payment.bolt11)) {
|
|
1484
|
+
throw cheat("invoice_not_issued");
|
|
1485
|
+
}
|
|
1486
|
+
if (askedAt !== void 0 && issued.settled === true) {
|
|
1487
|
+
throw cheat("invoice_settled");
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function namesTheAddress(metadata, address) {
|
|
1491
|
+
try {
|
|
1492
|
+
const entries = JSON.parse(metadata);
|
|
1493
|
+
return Array.isArray(entries) && entries.some(
|
|
1494
|
+
(entry) => Array.isArray(entry) && NAMES_AN_ADDRESS.includes(entry[0]) && typeof entry[1] === "string" && equalIgnoringCase(entry[1], address)
|
|
1495
|
+
);
|
|
1496
|
+
} catch {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async function proveSettlement(payment, asked) {
|
|
1501
|
+
await proveOrigin(payment, asked);
|
|
1502
|
+
const verified = await reachable(payment.verifyUrl, payment);
|
|
1503
|
+
if (verified.settled !== true || typeof verified.preimage !== "string") {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
if (!preimageMatchesHash(verified.preimage, payment.paymentHash)) {
|
|
1507
|
+
throw new GatewayCheatError("preimage_mismatch", payment.id);
|
|
1508
|
+
}
|
|
1509
|
+
return verified.preimage;
|
|
1510
|
+
}
|
|
1511
|
+
function carriesProof(report) {
|
|
1512
|
+
if (report.status !== "paid" || report.preimage === null) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
if (report.bolt11 !== void 0 && report.bolt11 !== null) {
|
|
1516
|
+
const invoiceHash = decodeInvoice(report.bolt11).paymentHash;
|
|
1517
|
+
if (invoiceHash === null || !equalIgnoringCase(invoiceHash, report.paymentHash)) {
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return preimageMatchesHash(report.preimage, report.paymentHash);
|
|
1522
|
+
}
|
|
1523
|
+
function wrapFeeCeiling(amountMsat, allowance) {
|
|
1524
|
+
const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
|
|
1525
|
+
return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
|
|
1526
|
+
}
|
|
1527
|
+
function proveWrapped(wrapped, recipient, allowance) {
|
|
1528
|
+
const offered = decodeInvoice(wrapped);
|
|
1529
|
+
const owed = decodeInvoice(recipient);
|
|
1530
|
+
const refuse2 = (code, detail) => {
|
|
1531
|
+
throw new WrapRefusedError(code, detail);
|
|
1532
|
+
};
|
|
1533
|
+
if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
|
|
1534
|
+
refuse2("undecodable", "the wrapped invoice carries no hash, amount or expiry");
|
|
1535
|
+
}
|
|
1536
|
+
if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
|
|
1537
|
+
refuse2("undecodable", "the recipient's invoice carries no hash, amount or expiry");
|
|
1538
|
+
}
|
|
1539
|
+
if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
|
|
1540
|
+
refuse2("hash_mismatch", "settling the wrap would not settle the recipient");
|
|
1541
|
+
}
|
|
1542
|
+
const asked = offered.amountMsat ?? 0;
|
|
1543
|
+
const due = owed.amountMsat ?? 0;
|
|
1544
|
+
if (asked < due) {
|
|
1545
|
+
refuse2("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
|
|
1546
|
+
}
|
|
1547
|
+
const ceiling = wrapFeeCeiling(due, allowance);
|
|
1548
|
+
if (asked - due > ceiling) {
|
|
1549
|
+
refuse2("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
|
|
1550
|
+
}
|
|
1551
|
+
if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
|
|
1552
|
+
refuse2("recipient_expires_first", "the operator could be paid and find it cannot deliver");
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function payRequestFor(listed2, payment) {
|
|
1556
|
+
const at = listed2.indexOf("@");
|
|
1557
|
+
const name = listed2.slice(0, at);
|
|
1558
|
+
const domain = listed2.slice(at + 1).toLowerCase();
|
|
1559
|
+
if (at < 1 || !publicHttps(`https://${domain}/`)) {
|
|
1560
|
+
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "not a usable address");
|
|
1561
|
+
}
|
|
1562
|
+
return reachable(`https://${domain}/.well-known/lnurlp/${name}`, payment);
|
|
1563
|
+
}
|
|
1564
|
+
async function reachable(url, payment) {
|
|
1565
|
+
try {
|
|
1566
|
+
if (!publicHttps(url)) {
|
|
1567
|
+
throw new Error(`${url} is not a public https URL`);
|
|
1568
|
+
}
|
|
1569
|
+
const response = await fetch(url, {
|
|
1570
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2),
|
|
1571
|
+
headers: { accept: "application/json" }
|
|
1572
|
+
});
|
|
1573
|
+
if (response.redirected && !sameOrigin(response.url, url)) {
|
|
1574
|
+
throw new Error(`redirected off its own origin, to ${response.url}`);
|
|
1575
|
+
}
|
|
1576
|
+
if (!response.ok) {
|
|
1577
|
+
throw new Error(`answered ${response.status}`);
|
|
1578
|
+
}
|
|
1579
|
+
const body = JSON.parse(await boundedText(response));
|
|
1580
|
+
if (body === null || typeof body !== "object") {
|
|
1581
|
+
throw new Error("answered with no JSON object");
|
|
1582
|
+
}
|
|
1583
|
+
return body;
|
|
1584
|
+
} catch (cause) {
|
|
1585
|
+
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, cause);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
async function boundedText(response) {
|
|
1589
|
+
const reader = response.body?.getReader();
|
|
1590
|
+
const decoder = new TextDecoder();
|
|
1591
|
+
let text3 = "";
|
|
1592
|
+
let bytes = 0;
|
|
1593
|
+
for (let chunk = await reader?.read(); chunk && !chunk.done; chunk = await reader?.read()) {
|
|
1594
|
+
bytes += chunk.value.length;
|
|
1595
|
+
if (bytes > BODY_LIMIT_BYTES) {
|
|
1596
|
+
await reader?.cancel();
|
|
1597
|
+
throw new Error(`answered with more than the ${BODY_LIMIT_BYTES} bytes a proof reads`);
|
|
1598
|
+
}
|
|
1599
|
+
text3 += decoder.decode(chunk.value, { stream: true });
|
|
1600
|
+
}
|
|
1601
|
+
return text3 + decoder.decode();
|
|
1602
|
+
}
|
|
1603
|
+
function equalIgnoringCase(one, other) {
|
|
1604
|
+
return one.toLowerCase() === other.toLowerCase();
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1282
1607
|
// src/wire.ts
|
|
1283
1608
|
var ASSET_CODE = "BTC";
|
|
1284
1609
|
var ASSET_SCALE = 11;
|
|
@@ -1484,11 +1809,11 @@ var CHALLENGE = "webhook-challenge";
|
|
|
1484
1809
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1485
1810
|
async function readPayment(request, credential, options = {}) {
|
|
1486
1811
|
const body = await believable(request, credential, options);
|
|
1487
|
-
return body === null ? null : decoded(body, paymentFromWire);
|
|
1812
|
+
return body === null ? null : proved(decoded(body, paymentFromWire));
|
|
1488
1813
|
}
|
|
1489
1814
|
async function readSettlement(request, credential, options = {}) {
|
|
1490
1815
|
const body = await believable(request, credential, options);
|
|
1491
|
-
return body === null ? null : decoded(body, settlementFromWire);
|
|
1816
|
+
return body === null ? null : proved(decoded(body, settlementFromWire));
|
|
1492
1817
|
}
|
|
1493
1818
|
async function answerWebhookChallenge(request, credential, options = {}) {
|
|
1494
1819
|
const body = await believable(request, credential, options);
|
|
@@ -1522,6 +1847,9 @@ async function believable(request, credential, options) {
|
|
|
1522
1847
|
);
|
|
1523
1848
|
return signed ? body : null;
|
|
1524
1849
|
}
|
|
1850
|
+
function proved(read) {
|
|
1851
|
+
return read !== null && read.status === "paid" && !carriesProof(read) ? null : read;
|
|
1852
|
+
}
|
|
1525
1853
|
function decoded(body, from) {
|
|
1526
1854
|
try {
|
|
1527
1855
|
return from(JSON.parse(body));
|
|
@@ -1562,30 +1890,39 @@ var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
|
1562
1890
|
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1563
1891
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1564
1892
|
var FORBIDDEN_IN_SPD = "*";
|
|
1893
|
+
var MAX_REFERENCE_CHARS = 60;
|
|
1565
1894
|
async function bankTransfer(gateway, params) {
|
|
1566
1895
|
const currency = params.currency ?? DEFAULT_CURRENCY;
|
|
1567
|
-
|
|
1896
|
+
const iban = accountOf(params.iban);
|
|
1897
|
+
refuseUnusable(params, iban, currency);
|
|
1568
1898
|
await refuseAnOpenGateway(gateway, params);
|
|
1569
|
-
const spd = shortPaymentDescriptor(params, currency);
|
|
1570
|
-
const subject = subjectOf(
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1899
|
+
const spd = shortPaymentDescriptor(params, iban, currency);
|
|
1900
|
+
const subject = subjectOf({
|
|
1901
|
+
iban,
|
|
1902
|
+
amountMinor: params.amountMinor,
|
|
1903
|
+
currency,
|
|
1904
|
+
expiresAt: params.expiresAt,
|
|
1905
|
+
reference: params.reference
|
|
1906
|
+
});
|
|
1907
|
+
const verifyUrl = await answeredAt(params, subject);
|
|
1908
|
+
const paymentHash = hashOf(await hmacHex(params.secret, `preimage|${subject}`));
|
|
1577
1909
|
const watched = await gateway.watch({
|
|
1578
|
-
paymentHash
|
|
1910
|
+
paymentHash,
|
|
1579
1911
|
verifyUrl,
|
|
1580
1912
|
expiresAt: params.expiresAt,
|
|
1581
1913
|
trigger: params.trigger,
|
|
1582
1914
|
replay: params.replay,
|
|
1583
|
-
sealed: params.sealed,
|
|
1915
|
+
sealed: params.sealed ? await seal(params.sealed.secret, JSON.stringify(params.sealed.data), paymentHash) : void 0,
|
|
1584
1916
|
webhookUrl: params.webhookUrl
|
|
1585
1917
|
});
|
|
1586
|
-
return { id: watched.id, paymentHash
|
|
1918
|
+
return { id: watched.id, paymentHash, verifyUrl, spd };
|
|
1587
1919
|
}
|
|
1588
1920
|
function bankVerifyEndpoint(config) {
|
|
1921
|
+
refuseAWeakSecret(config.secret);
|
|
1922
|
+
const answersFor = accountOf(config.iban);
|
|
1923
|
+
if (!IBAN.test(answersFor)) {
|
|
1924
|
+
throw new Error(`${config.iban} is not an IBAN, so this endpoint answers for no account`);
|
|
1925
|
+
}
|
|
1589
1926
|
const paced = {
|
|
1590
1927
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1591
1928
|
};
|
|
@@ -1594,51 +1931,79 @@ function bankVerifyEndpoint(config) {
|
|
|
1594
1931
|
if (consented !== null) {
|
|
1595
1932
|
return consented;
|
|
1596
1933
|
}
|
|
1597
|
-
const
|
|
1598
|
-
if (
|
|
1934
|
+
const sealed = new URL(request.url).searchParams.get("q");
|
|
1935
|
+
if (sealed === null) {
|
|
1599
1936
|
return Response.json({ settled: false }, { status: 400 });
|
|
1600
1937
|
}
|
|
1601
|
-
const subject =
|
|
1602
|
-
const
|
|
1603
|
-
if (
|
|
1938
|
+
const subject = await unseal(config.secret, sealed);
|
|
1939
|
+
const asked = subject === null ? null : askedFrom(subject);
|
|
1940
|
+
if (asked === null || asked.iban !== answersFor) {
|
|
1604
1941
|
return Response.json({ settled: false }, { status: 403 });
|
|
1605
1942
|
}
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
{ headers: paced }
|
|
1614
|
-
);
|
|
1943
|
+
const preimage = await creditedPreimage({
|
|
1944
|
+
secret: config.secret,
|
|
1945
|
+
asked,
|
|
1946
|
+
statement: config.statement,
|
|
1947
|
+
lookBackSecs: config.lookBackSecs
|
|
1948
|
+
});
|
|
1949
|
+
return preimage === null ? Response.json({ settled: false }, { headers: paced }) : Response.json({ settled: true, preimage }, { headers: paced });
|
|
1615
1950
|
};
|
|
1616
1951
|
}
|
|
1617
|
-
function
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1952
|
+
async function answeredAt(params, subject) {
|
|
1953
|
+
if (params.answerBy === "agent") {
|
|
1954
|
+
return `agent:${(await callerKey(params.secret)).publicKeyHex}`;
|
|
1955
|
+
}
|
|
1956
|
+
const polling = new URL(params.verifyUrl ?? "");
|
|
1957
|
+
polling.searchParams.set("q", await sealStable(params.secret, subject));
|
|
1958
|
+
return polling.toString();
|
|
1959
|
+
}
|
|
1960
|
+
async function creditedPreimage(params) {
|
|
1961
|
+
const since = unixNow() - (params.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
1962
|
+
const landed = (await params.statement(since)).some((credit) => pays(credit, params.asked));
|
|
1963
|
+
if (!landed) {
|
|
1623
1964
|
return null;
|
|
1624
1965
|
}
|
|
1625
|
-
|
|
1966
|
+
return await hmacHex(params.secret, `preimage|${subjectOf(params.asked)}`);
|
|
1967
|
+
}
|
|
1968
|
+
function askedFrom(subject) {
|
|
1969
|
+
const [iban, minorText, currency, expiresText, ...rest] = subject.split("|");
|
|
1970
|
+
const reference = rest.join("|");
|
|
1971
|
+
const amountMinor = Number(minorText);
|
|
1972
|
+
const expiresAt = Number(expiresText);
|
|
1973
|
+
if (!iban || !currency || reference.length === 0) {
|
|
1626
1974
|
return null;
|
|
1627
1975
|
}
|
|
1628
|
-
|
|
1976
|
+
if (!Number.isInteger(amountMinor) || amountMinor <= 0 || !Number.isSafeInteger(expiresAt)) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
return { iban, reference, amountMinor, currency, expiresAt };
|
|
1629
1980
|
}
|
|
1630
1981
|
function pays(credit, asked) {
|
|
1631
|
-
return credit.amountMinor === asked.amountMinor && credit.currency.toUpperCase() === asked.currency.toUpperCase() && credit.reference
|
|
1982
|
+
return credit.amountMinor === asked.amountMinor && credit.currency.toUpperCase() === asked.currency.toUpperCase() && namesOnly(credit.reference, asked.reference);
|
|
1983
|
+
}
|
|
1984
|
+
function namesOnly(written, reference) {
|
|
1985
|
+
const wanted = reference.toUpperCase();
|
|
1986
|
+
const alike = [...written.toUpperCase().matchAll(wordsShapedLike(wanted))].map(([word]) => word);
|
|
1987
|
+
return alike.includes(wanted) && (!/\d/.test(wanted) || alike.every((word) => word === wanted));
|
|
1632
1988
|
}
|
|
1633
|
-
function
|
|
1634
|
-
|
|
1989
|
+
function wordsShapedLike(reference) {
|
|
1990
|
+
const shape = [...reference].map((char) => {
|
|
1991
|
+
if (/\d/.test(char)) {
|
|
1992
|
+
return "\\d";
|
|
1993
|
+
}
|
|
1994
|
+
return /\p{L}/u.test(char) ? "\\p{L}" : char.replace(/[\^$\\.*+?()[\]{}|/]/g, "\\$&");
|
|
1995
|
+
}).join("");
|
|
1996
|
+
return new RegExp(`(?<![\\p{L}\\p{N}])${shape}(?![\\p{L}\\p{N}])`, "gu");
|
|
1997
|
+
}
|
|
1998
|
+
function subjectOf({ iban, amountMinor, currency, expiresAt, reference }) {
|
|
1999
|
+
return `${iban}|${amountMinor}|${currency.toUpperCase()}|${expiresAt}|${reference}`;
|
|
1635
2000
|
}
|
|
1636
2001
|
function hashOf(preimage) {
|
|
1637
2002
|
return bytesToHex(sha256(hexToBytes(preimage)));
|
|
1638
2003
|
}
|
|
1639
|
-
function shortPaymentDescriptor(params, currency) {
|
|
2004
|
+
function shortPaymentDescriptor(params, iban, currency) {
|
|
1640
2005
|
const fields = [
|
|
1641
|
-
`ACC:${
|
|
2006
|
+
`ACC:${iban}`,
|
|
1642
2007
|
`AM:${major(params.amountMinor, currency)}`,
|
|
1643
2008
|
`CC:${currency.toUpperCase()}`,
|
|
1644
2009
|
`MSG:${params.reference}`
|
|
@@ -1659,19 +2024,28 @@ async function refuseAnOpenGateway(gateway, params) {
|
|
|
1659
2024
|
return;
|
|
1660
2025
|
}
|
|
1661
2026
|
throw new Error(
|
|
1662
|
-
"this gateway serves callers with no token, so it is not yours, and its operator would
|
|
2027
|
+
"this gateway serves callers with no token, so it is not yours, and its operator would learn every watch you place. Point at one that answers 401 to a stranger, or say allowPublicGateway"
|
|
1663
2028
|
);
|
|
1664
2029
|
}
|
|
1665
|
-
function refuseUnusable(params, currency) {
|
|
1666
|
-
|
|
2030
|
+
function refuseUnusable(params, iban, currency) {
|
|
2031
|
+
refuseAWeakSecret(params.secret);
|
|
2032
|
+
if (params.answerBy !== "agent" && params.verifyUrl === void 0) {
|
|
2033
|
+
throw new Error("a transfer the gateway polls needs the verify url it is polled at");
|
|
2034
|
+
}
|
|
2035
|
+
if (!IBAN.test(iban)) {
|
|
1667
2036
|
throw new Error(`${params.iban} is not an IBAN`);
|
|
1668
2037
|
}
|
|
1669
|
-
if (!Number.
|
|
2038
|
+
if (!Number.isSafeInteger(params.amountMinor) || params.amountMinor <= 0) {
|
|
1670
2039
|
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
1671
2040
|
}
|
|
1672
2041
|
if (params.reference.length === 0) {
|
|
1673
2042
|
throw new Error("a transfer with no reference cannot be found");
|
|
1674
2043
|
}
|
|
2044
|
+
if (params.reference.length > MAX_REFERENCE_CHARS) {
|
|
2045
|
+
throw new Error(
|
|
2046
|
+
`a reference is at most ${MAX_REFERENCE_CHARS} characters, as many as a QR payment's message carries and its sealed query hides`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
1675
2049
|
if (params.reference.includes(FORBIDDEN_IN_SPD)) {
|
|
1676
2050
|
throw new Error("a reference cannot contain an asterisk, it separates the QR fields");
|
|
1677
2051
|
}
|
|
@@ -1679,6 +2053,9 @@ function refuseUnusable(params, currency) {
|
|
|
1679
2053
|
throw new Error("a variable symbol is up to ten digits");
|
|
1680
2054
|
}
|
|
1681
2055
|
}
|
|
2056
|
+
function accountOf(iban) {
|
|
2057
|
+
return iban.replace(/\s+/g, "").toUpperCase();
|
|
2058
|
+
}
|
|
1682
2059
|
function unixNow() {
|
|
1683
2060
|
return Math.floor(Date.now() / 1e3);
|
|
1684
2061
|
}
|
|
@@ -1742,10 +2119,10 @@ function lightningVerifyEndpoint(config) {
|
|
|
1742
2119
|
return Response.json({ settled: false }, { status: 400 });
|
|
1743
2120
|
}
|
|
1744
2121
|
const opened = await unseal(config.secret, sealed);
|
|
1745
|
-
|
|
2122
|
+
const wallet = opened === null ? null : relayedFrom(opened);
|
|
2123
|
+
if (wallet === null) {
|
|
1746
2124
|
return Response.json({ settled: false }, { status: 403 });
|
|
1747
2125
|
}
|
|
1748
|
-
const wallet = JSON.parse(opened);
|
|
1749
2126
|
const asked = await checkSettled(
|
|
1750
2127
|
config.send ?? pinnedToTheAddressWeVerified,
|
|
1751
2128
|
wallet.url,
|
|
@@ -1760,6 +2137,14 @@ function lightningVerifyEndpoint(config) {
|
|
|
1760
2137
|
);
|
|
1761
2138
|
};
|
|
1762
2139
|
}
|
|
2140
|
+
function relayedFrom(opened) {
|
|
2141
|
+
try {
|
|
2142
|
+
const said = JSON.parse(opened);
|
|
2143
|
+
return typeof said?.url === "string" && typeof said.hash === "string" ? { url: said.url, hash: said.hash } : null;
|
|
2144
|
+
} catch {
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
1763
2148
|
async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
1764
2149
|
const relayed = new URL(endpoint);
|
|
1765
2150
|
relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
|
|
@@ -1782,13 +2167,14 @@ function bankRail(gateway, config) {
|
|
|
1782
2167
|
expiresAt,
|
|
1783
2168
|
trigger: config.trigger,
|
|
1784
2169
|
replay: config.replay,
|
|
1785
|
-
sealed:
|
|
2170
|
+
sealed: config.sealed && { secret: config.sealed.secret, data: config.sealed.data(order) },
|
|
1786
2171
|
variableSymbol: config.variableSymbol?.(order),
|
|
1787
2172
|
webhookUrl: config.webhookUrl,
|
|
1788
2173
|
allowPublicGateway: config.allowPublicGateway
|
|
1789
2174
|
});
|
|
1790
2175
|
return {
|
|
1791
2176
|
id: transfer.id,
|
|
2177
|
+
paymentHash: transfer.paymentHash,
|
|
1792
2178
|
rail: config.name ?? BANK,
|
|
1793
2179
|
scan: transfer.spd,
|
|
1794
2180
|
qr: transfer.spd,
|
|
@@ -1812,6 +2198,7 @@ function lightningRail(gateway, config) {
|
|
|
1812
2198
|
);
|
|
1813
2199
|
return {
|
|
1814
2200
|
id: payment.id,
|
|
2201
|
+
paymentHash: payment.paymentHash,
|
|
1815
2202
|
rail: config.name ?? LIGHTNING,
|
|
1816
2203
|
scan: payment.bolt11,
|
|
1817
2204
|
qr: toLightningUri(payment.bolt11),
|
|
@@ -1837,11 +2224,16 @@ function blindLightningRail(gateway, config) {
|
|
|
1837
2224
|
expiresAt: resolved2.expiresAt,
|
|
1838
2225
|
trigger: config.trigger,
|
|
1839
2226
|
replay: config.replay,
|
|
1840
|
-
sealed:
|
|
2227
|
+
sealed: config.sealed ? await seal(
|
|
2228
|
+
config.sealed.secret,
|
|
2229
|
+
JSON.stringify(config.sealed.data(order)),
|
|
2230
|
+
resolved2.paymentHash
|
|
2231
|
+
) : void 0,
|
|
1841
2232
|
webhookUrl: config.webhookUrl
|
|
1842
2233
|
});
|
|
1843
2234
|
return {
|
|
1844
2235
|
id: watched.id,
|
|
2236
|
+
paymentHash: resolved2.paymentHash,
|
|
1845
2237
|
rail: config.name ?? LIGHTNING,
|
|
1846
2238
|
scan: resolved2.bolt11,
|
|
1847
2239
|
qr: toLightningUri(resolved2.bolt11),
|
|
@@ -1895,130 +2287,6 @@ var Rails = class {
|
|
|
1895
2287
|
}
|
|
1896
2288
|
};
|
|
1897
2289
|
|
|
1898
|
-
// src/verify.ts
|
|
1899
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1900
|
-
var DEFAULT_WRAP_PROPORTION = 0.01;
|
|
1901
|
-
var DEFAULT_WRAP_BASE_MSAT = 1e3;
|
|
1902
|
-
async function proveOrigin(payment, asked) {
|
|
1903
|
-
const cheat = (code) => new GatewayCheatError(code, payment.id);
|
|
1904
|
-
const listed = asked.paidTo.find((address) => equalIgnoringCase(address, payment.lnAddress));
|
|
1905
|
-
if (listed === void 0) {
|
|
1906
|
-
throw cheat("address_not_requested");
|
|
1907
|
-
}
|
|
1908
|
-
if (payment.amountMsat !== asked.amountMsat) {
|
|
1909
|
-
throw cheat("amount_mismatch");
|
|
1910
|
-
}
|
|
1911
|
-
const invoice = decodeInvoice(payment.bolt11);
|
|
1912
|
-
if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
|
|
1913
|
-
throw cheat("hash_mismatch");
|
|
1914
|
-
}
|
|
1915
|
-
if (invoice.amountMsat !== asked.amountMsat) {
|
|
1916
|
-
throw cheat("amount_mismatch");
|
|
1917
|
-
}
|
|
1918
|
-
const payRequest = await payRequestFor(listed, payment);
|
|
1919
|
-
if (typeof payRequest.metadata !== "string" || typeof payRequest.callback !== "string") {
|
|
1920
|
-
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "no payRequest served");
|
|
1921
|
-
}
|
|
1922
|
-
if (invoice.descriptionHash !== sha256Hex(payRequest.metadata)) {
|
|
1923
|
-
throw cheat("description_hash_mismatch");
|
|
1924
|
-
}
|
|
1925
|
-
if (!sameOrigin(payment.verifyUrl, payRequest.callback)) {
|
|
1926
|
-
throw cheat("verify_url_foreign");
|
|
1927
|
-
}
|
|
1928
|
-
const issued = await reachable(payment.verifyUrl, payment);
|
|
1929
|
-
if (typeof issued.pr !== "string" || !equalIgnoringCase(issued.pr, payment.bolt11)) {
|
|
1930
|
-
throw cheat("invoice_not_issued");
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
async function proveSettlement(payment, asked) {
|
|
1934
|
-
await proveOrigin(payment, asked);
|
|
1935
|
-
const verified = await reachable(payment.verifyUrl, payment);
|
|
1936
|
-
if (verified.settled !== true || typeof verified.preimage !== "string") {
|
|
1937
|
-
return null;
|
|
1938
|
-
}
|
|
1939
|
-
if (!preimageMatchesHash(verified.preimage, payment.paymentHash)) {
|
|
1940
|
-
throw new GatewayCheatError("preimage_mismatch", payment.id);
|
|
1941
|
-
}
|
|
1942
|
-
return verified.preimage;
|
|
1943
|
-
}
|
|
1944
|
-
function carriesProof(report) {
|
|
1945
|
-
if (report.status !== "paid" || report.preimage === null) {
|
|
1946
|
-
return false;
|
|
1947
|
-
}
|
|
1948
|
-
if (report.bolt11 !== void 0 && report.bolt11 !== null) {
|
|
1949
|
-
const invoiceHash = decodeInvoice(report.bolt11).paymentHash;
|
|
1950
|
-
if (invoiceHash === null || !equalIgnoringCase(invoiceHash, report.paymentHash)) {
|
|
1951
|
-
return false;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
return preimageMatchesHash(report.preimage, report.paymentHash);
|
|
1955
|
-
}
|
|
1956
|
-
function wrapFeeCeiling(amountMsat, allowance) {
|
|
1957
|
-
const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
|
|
1958
|
-
return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
|
|
1959
|
-
}
|
|
1960
|
-
function proveWrapped(wrapped, recipient, allowance) {
|
|
1961
|
-
const offered = decodeInvoice(wrapped);
|
|
1962
|
-
const owed = decodeInvoice(recipient);
|
|
1963
|
-
const refuse2 = (code, detail) => {
|
|
1964
|
-
throw new WrapRefusedError(code, detail);
|
|
1965
|
-
};
|
|
1966
|
-
if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
|
|
1967
|
-
refuse2("undecodable", "the wrapped invoice carries no hash, amount or expiry");
|
|
1968
|
-
}
|
|
1969
|
-
if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
|
|
1970
|
-
refuse2("undecodable", "the recipient's invoice carries no hash, amount or expiry");
|
|
1971
|
-
}
|
|
1972
|
-
if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
|
|
1973
|
-
refuse2("hash_mismatch", "settling the wrap would not settle the recipient");
|
|
1974
|
-
}
|
|
1975
|
-
const asked = offered.amountMsat ?? 0;
|
|
1976
|
-
const due = owed.amountMsat ?? 0;
|
|
1977
|
-
if (asked < due) {
|
|
1978
|
-
refuse2("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
|
|
1979
|
-
}
|
|
1980
|
-
const ceiling = wrapFeeCeiling(due, allowance);
|
|
1981
|
-
if (asked - due > ceiling) {
|
|
1982
|
-
refuse2("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
|
|
1983
|
-
}
|
|
1984
|
-
if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
|
|
1985
|
-
refuse2("recipient_expires_first", "the operator could be paid and find it cannot deliver");
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
async function payRequestFor(listed, payment) {
|
|
1989
|
-
const at = listed.indexOf("@");
|
|
1990
|
-
const name = listed.slice(0, at);
|
|
1991
|
-
const domain = listed.slice(at + 1).toLowerCase();
|
|
1992
|
-
if (at < 1 || !publicHttps(`https://${domain}/`)) {
|
|
1993
|
-
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, "not a usable address");
|
|
1994
|
-
}
|
|
1995
|
-
return reachable(`https://${domain}/.well-known/lnurlp/${name}`, payment);
|
|
1996
|
-
}
|
|
1997
|
-
async function reachable(url, payment) {
|
|
1998
|
-
try {
|
|
1999
|
-
if (!publicHttps(url)) {
|
|
2000
|
-
throw new Error(`${url} is not a public https URL`);
|
|
2001
|
-
}
|
|
2002
|
-
const response = await fetch(url, {
|
|
2003
|
-
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2),
|
|
2004
|
-
headers: { accept: "application/json" }
|
|
2005
|
-
});
|
|
2006
|
-
if (!response.ok) {
|
|
2007
|
-
throw new Error(`answered ${response.status}`);
|
|
2008
|
-
}
|
|
2009
|
-
const body = await response.json();
|
|
2010
|
-
if (body === null || typeof body !== "object") {
|
|
2011
|
-
throw new Error("answered with no JSON object");
|
|
2012
|
-
}
|
|
2013
|
-
return body;
|
|
2014
|
-
} catch (cause) {
|
|
2015
|
-
throw new UnverifiedRecipientError(payment.lnAddress, payment.id, cause);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
function equalIgnoringCase(one, other) {
|
|
2019
|
-
return one.toLowerCase() === other.toLowerCase();
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
2290
|
// src/request.ts
|
|
2023
2291
|
function paymentRequestOf(gateway, payment, options) {
|
|
2024
2292
|
return {
|
|
@@ -2030,13 +2298,13 @@ function paymentRequestOf(gateway, payment, options) {
|
|
|
2030
2298
|
expiresAt: payment.expiresAt,
|
|
2031
2299
|
qr: invoiceToSvg(payment.bolt11, options?.qr),
|
|
2032
2300
|
payment,
|
|
2033
|
-
paid: async (waiting) => paidOnly(await gateway.settled(payment
|
|
2301
|
+
paid: async (waiting) => paidOnly(await gateway.settled(payment, { ...waitingOf(options), ...waiting })),
|
|
2034
2302
|
prove: () => proveSettlement(payment, { paidTo: [payment.lnAddress], amountMsat: payment.amountMsat }),
|
|
2035
2303
|
onPaid: (arrived, failed) => {
|
|
2036
2304
|
const stop = new AbortController();
|
|
2037
2305
|
const waiting = waitingOf(options);
|
|
2038
2306
|
const signal = waiting.signal ? AbortSignal.any([stop.signal, waiting.signal]) : stop.signal;
|
|
2039
|
-
gateway.settled(payment
|
|
2307
|
+
gateway.settled(payment, { ...waiting, signal }).then((ended) => arrived(paidOnly(ended))).catch((reason) => {
|
|
2040
2308
|
if (!stop.signal.aborted) {
|
|
2041
2309
|
failed?.(reason);
|
|
2042
2310
|
}
|
|
@@ -2085,22 +2353,37 @@ function publicWatchTicketEndpoint(gateway, config) {
|
|
|
2085
2353
|
}
|
|
2086
2354
|
async function offer(gateway, config, url) {
|
|
2087
2355
|
const { least, most } = await spread(config.amount);
|
|
2088
|
-
const
|
|
2356
|
+
const winner = await quoted(gateway, config, least);
|
|
2089
2357
|
const nonce = randomNonce();
|
|
2090
2358
|
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
2091
|
-
callback.searchParams.set("to",
|
|
2359
|
+
callback.searchParams.set("to", winner.address);
|
|
2092
2360
|
callback.searchParams.set("least", String(least));
|
|
2093
2361
|
callback.searchParams.set("most", String(most));
|
|
2094
2362
|
callback.searchParams.set("n", nonce);
|
|
2095
|
-
callback.searchParams.set("sig", await sign(config.secret,
|
|
2363
|
+
callback.searchParams.set("sig", await sign(config.secret, winner.address, least, most, nonce));
|
|
2096
2364
|
return Response.json({
|
|
2097
2365
|
tag: "payRequest",
|
|
2098
2366
|
callback: callback.toString(),
|
|
2099
|
-
metadata:
|
|
2367
|
+
metadata: winner.metadata,
|
|
2100
2368
|
minSendable: least,
|
|
2101
2369
|
maxSendable: most
|
|
2102
2370
|
});
|
|
2103
2371
|
}
|
|
2372
|
+
async function quoted(gateway, config, amountMsat) {
|
|
2373
|
+
if (config.blind) {
|
|
2374
|
+
const { won } = await quote(
|
|
2375
|
+
config.send ?? pinnedToTheAddressWeVerified,
|
|
2376
|
+
listed(config.paidTo),
|
|
2377
|
+
amountMsat
|
|
2378
|
+
);
|
|
2379
|
+
return won;
|
|
2380
|
+
}
|
|
2381
|
+
const { lnAddress, metadata } = await gateway.quote({
|
|
2382
|
+
paidTo: config.paidTo,
|
|
2383
|
+
amount: msat(amountMsat)
|
|
2384
|
+
});
|
|
2385
|
+
return { address: lnAddress, metadata };
|
|
2386
|
+
}
|
|
2104
2387
|
async function spread(amount) {
|
|
2105
2388
|
if (typeof amount === "number" || typeof amount === "function") {
|
|
2106
2389
|
const pinned = await amountNow(amount);
|
|
@@ -2122,6 +2405,9 @@ async function mint(gateway, config, url, address) {
|
|
|
2122
2405
|
if (!equalInConstantTime(signature, expected)) {
|
|
2123
2406
|
return refuse("this callback was not signed here");
|
|
2124
2407
|
}
|
|
2408
|
+
if (!listed(config.paidTo).some((one) => one.toLowerCase() === address.toLowerCase())) {
|
|
2409
|
+
return refuse(`${address} is not paid through this trigger`);
|
|
2410
|
+
}
|
|
2125
2411
|
const asked = url.searchParams.get("amount");
|
|
2126
2412
|
const amountMsat = asked === null ? least : Number(asked);
|
|
2127
2413
|
if (!Number.isSafeInteger(amountMsat) || amountMsat < least || amountMsat > most) {
|
|
@@ -2164,7 +2450,7 @@ async function mintBlind(gateway, config, address, amountMsat) {
|
|
|
2164
2450
|
expiresAt: resolved2.expiresAt,
|
|
2165
2451
|
trigger: config.watchSecret,
|
|
2166
2452
|
replay: config.replay,
|
|
2167
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
2453
|
+
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted)), minted.paymentHash) : void 0
|
|
2168
2454
|
});
|
|
2169
2455
|
} catch (refused) {
|
|
2170
2456
|
if (!alreadyWatched(refused)) {
|
|
@@ -2173,6 +2459,9 @@ async function mintBlind(gateway, config, address, amountMsat) {
|
|
|
2173
2459
|
}
|
|
2174
2460
|
return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
|
|
2175
2461
|
}
|
|
2462
|
+
function listed(paidTo) {
|
|
2463
|
+
return typeof paidTo === "string" ? [paidTo] : paidTo;
|
|
2464
|
+
}
|
|
2176
2465
|
function alreadyWatched(refused) {
|
|
2177
2466
|
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
2178
2467
|
}
|
|
@@ -2312,6 +2601,21 @@ function backoffMs(firstDelay, attempt) {
|
|
|
2312
2601
|
const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
|
|
2313
2602
|
return Math.round(grown * (0.5 + Math.random() / 2));
|
|
2314
2603
|
}
|
|
2604
|
+
function askFromWire(raw) {
|
|
2605
|
+
let said;
|
|
2606
|
+
try {
|
|
2607
|
+
said = JSON.parse(raw);
|
|
2608
|
+
} catch {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
if (typeof said !== "object" || said === null) {
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
const fields = said;
|
|
2615
|
+
const ask2 = fields["ask"];
|
|
2616
|
+
const paymentHash = fields["payment_hash"];
|
|
2617
|
+
return typeof ask2 === "string" && typeof paymentHash === "string" ? { ask: ask2, paymentHash } : null;
|
|
2618
|
+
}
|
|
2315
2619
|
function gatewayAt(baseUrl) {
|
|
2316
2620
|
const at = URL.canParse(baseUrl) ? new URL(baseUrl) : null;
|
|
2317
2621
|
if (at === null || at.protocol !== "http:" && at.protocol !== "https:") {
|
|
@@ -2321,8 +2625,8 @@ function gatewayAt(baseUrl) {
|
|
|
2321
2625
|
}
|
|
2322
2626
|
var ThunderBridge = class {
|
|
2323
2627
|
baseUrl;
|
|
2324
|
-
verify;
|
|
2325
2628
|
token;
|
|
2629
|
+
handedOut = /* @__PURE__ */ new Map();
|
|
2326
2630
|
secret;
|
|
2327
2631
|
strangers = null;
|
|
2328
2632
|
speaks = null;
|
|
@@ -2333,7 +2637,6 @@ var ThunderBridge = class {
|
|
|
2333
2637
|
rails;
|
|
2334
2638
|
constructor(baseUrl, options) {
|
|
2335
2639
|
this.baseUrl = gatewayAt(baseUrl);
|
|
2336
|
-
this.verify = options?.verify ?? true;
|
|
2337
2640
|
this.token = options?.token ?? null;
|
|
2338
2641
|
this.secret = options?.secret ?? null;
|
|
2339
2642
|
this.serve = new Serve(this);
|
|
@@ -2375,6 +2678,7 @@ var ThunderBridge = class {
|
|
|
2375
2678
|
* and `GatewayCheatError` when what comes back is not what you asked for
|
|
2376
2679
|
*/
|
|
2377
2680
|
async mint(charge, options) {
|
|
2681
|
+
const askedAt = Math.floor(Date.now() / 1e3);
|
|
2378
2682
|
const asked = await priced(charge);
|
|
2379
2683
|
const sent = createRequestBody(
|
|
2380
2684
|
asked,
|
|
@@ -2395,9 +2699,8 @@ var ThunderBridge = class {
|
|
|
2395
2699
|
throw await problemFrom(response);
|
|
2396
2700
|
}
|
|
2397
2701
|
const payment = await mintedFrom(response);
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
}
|
|
2702
|
+
await proveOrigin(payment, asked, askedAt);
|
|
2703
|
+
this.handOut(payment, options?.idempotencyKey ?? null);
|
|
2401
2704
|
return payment;
|
|
2402
2705
|
}
|
|
2403
2706
|
/**
|
|
@@ -2408,7 +2711,8 @@ var ThunderBridge = class {
|
|
|
2408
2711
|
* it for one, and asking mints it
|
|
2409
2712
|
*/
|
|
2410
2713
|
async quote(charge) {
|
|
2411
|
-
const
|
|
2714
|
+
const asked = await priced(charge);
|
|
2715
|
+
const sent = quoteRequestBody(asked);
|
|
2412
2716
|
const response = await fetch(`${this.baseUrl}/quotes`, {
|
|
2413
2717
|
method: "POST",
|
|
2414
2718
|
headers: await this.sending("/quotes", sent),
|
|
@@ -2417,14 +2721,17 @@ var ThunderBridge = class {
|
|
|
2417
2721
|
if (!response.ok) {
|
|
2418
2722
|
throw await problemFrom(response);
|
|
2419
2723
|
}
|
|
2420
|
-
const
|
|
2421
|
-
if (
|
|
2724
|
+
const quote2 = quoteFromWire(await response.json().catch(() => null));
|
|
2725
|
+
if (quote2 === null) {
|
|
2422
2726
|
throw new ProblemError({
|
|
2423
2727
|
status: response.status,
|
|
2424
2728
|
title: "The gateway answered with something that is not a quote"
|
|
2425
2729
|
});
|
|
2426
2730
|
}
|
|
2427
|
-
|
|
2731
|
+
if (!asked.paidTo.some((address) => address.toLowerCase() === quote2.lnAddress.toLowerCase())) {
|
|
2732
|
+
throw new GatewayCheatError("address_not_requested", "");
|
|
2733
|
+
}
|
|
2734
|
+
return quote2;
|
|
2428
2735
|
}
|
|
2429
2736
|
/**
|
|
2430
2737
|
* The key this gateway signs webhooks with when you registered none of your own.
|
|
@@ -2457,8 +2764,8 @@ var ThunderBridge = class {
|
|
|
2457
2764
|
* for both sorts: `kind` says whether the gateway minted it or was handed it,
|
|
2458
2765
|
* and the address, amount and invoice are null on one it was never told
|
|
2459
2766
|
*/
|
|
2460
|
-
async payment(
|
|
2461
|
-
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
2767
|
+
async payment(held) {
|
|
2768
|
+
const path = `/incoming-payments/${encodeURIComponent(held.id)}`;
|
|
2462
2769
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2463
2770
|
headers: await this.reading("GET", path)
|
|
2464
2771
|
});
|
|
@@ -2468,7 +2775,7 @@ var ThunderBridge = class {
|
|
|
2468
2775
|
if (!response.ok) {
|
|
2469
2776
|
throw await problemFrom(response);
|
|
2470
2777
|
}
|
|
2471
|
-
return this.proven(await paymentFrom(response));
|
|
2778
|
+
return this.proven(await paymentFrom(response), held);
|
|
2472
2779
|
}
|
|
2473
2780
|
/**
|
|
2474
2781
|
* List what this gateway is watching, newest first. Only a gateway started
|
|
@@ -2488,14 +2795,14 @@ var ThunderBridge = class {
|
|
|
2488
2795
|
throw await problemFrom(response);
|
|
2489
2796
|
}
|
|
2490
2797
|
const body = await response.json().catch(() => null);
|
|
2491
|
-
const
|
|
2492
|
-
if (
|
|
2798
|
+
const listed2 = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null).filter((one) => one.status !== "paid" || carriesProof(one)) : null;
|
|
2799
|
+
if (listed2 === null || typeof body?.settled_scanned !== "number") {
|
|
2493
2800
|
throw new ProblemError({
|
|
2494
2801
|
status: response.status,
|
|
2495
2802
|
title: "The gateway answered with something that is not a payment list"
|
|
2496
2803
|
});
|
|
2497
2804
|
}
|
|
2498
|
-
return { payments:
|
|
2805
|
+
return { payments: listed2, scanned: body.settled_scanned };
|
|
2499
2806
|
}
|
|
2500
2807
|
/**
|
|
2501
2808
|
* Follow a payment over WebSocket until it is paid or expired, reconnecting
|
|
@@ -2503,15 +2810,15 @@ var ThunderBridge = class {
|
|
|
2503
2810
|
* one that has answered is followed until its own expiry, so the wait always
|
|
2504
2811
|
* ends by itself
|
|
2505
2812
|
*/
|
|
2506
|
-
async settled(
|
|
2507
|
-
const ended = paymentFromWire(await this.followed(id, options));
|
|
2813
|
+
async settled(held, options) {
|
|
2814
|
+
const ended = paymentFromWire(await this.followed(held.id, options));
|
|
2508
2815
|
if (ended === null) {
|
|
2509
2816
|
throw new ProblemError({
|
|
2510
2817
|
status: 200,
|
|
2511
2818
|
title: "The gateway answered with something that is not a payment"
|
|
2512
2819
|
});
|
|
2513
2820
|
}
|
|
2514
|
-
return this.proven(ended);
|
|
2821
|
+
return this.proven(ended, held);
|
|
2515
2822
|
}
|
|
2516
2823
|
followed(id, options) {
|
|
2517
2824
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
@@ -2616,8 +2923,8 @@ var ThunderBridge = class {
|
|
|
2616
2923
|
* really does pay twice and that shows up on `follow` as a second settlement to
|
|
2617
2924
|
* refund.
|
|
2618
2925
|
*/
|
|
2619
|
-
async firstSettled(
|
|
2620
|
-
if (
|
|
2926
|
+
async firstSettled(held, options) {
|
|
2927
|
+
if (held.length === 0) {
|
|
2621
2928
|
return null;
|
|
2622
2929
|
}
|
|
2623
2930
|
const stopLosers = new AbortController();
|
|
@@ -2625,15 +2932,15 @@ var ThunderBridge = class {
|
|
|
2625
2932
|
let refused = null;
|
|
2626
2933
|
try {
|
|
2627
2934
|
const winner = await new Promise((resolve2) => {
|
|
2628
|
-
let waiting =
|
|
2935
|
+
let waiting = held.length;
|
|
2629
2936
|
const lost = () => {
|
|
2630
2937
|
waiting -= 1;
|
|
2631
2938
|
if (waiting === 0) {
|
|
2632
2939
|
resolve2(null);
|
|
2633
2940
|
}
|
|
2634
2941
|
};
|
|
2635
|
-
for (const
|
|
2636
|
-
this.settled(
|
|
2942
|
+
for (const leg of held) {
|
|
2943
|
+
this.settled(leg, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
|
|
2637
2944
|
refused ??= failure;
|
|
2638
2945
|
lost();
|
|
2639
2946
|
});
|
|
@@ -2677,6 +2984,9 @@ var ThunderBridge = class {
|
|
|
2677
2984
|
if (named !== null && watched.id !== named) {
|
|
2678
2985
|
throw new GatewayCheatError("id_not_mine", watched.id);
|
|
2679
2986
|
}
|
|
2987
|
+
if (watched.paymentHash.toLowerCase() !== handover.paymentHash.toLowerCase()) {
|
|
2988
|
+
throw new GatewayCheatError("hash_mismatch", watched.id);
|
|
2989
|
+
}
|
|
2680
2990
|
return watched;
|
|
2681
2991
|
}
|
|
2682
2992
|
/**
|
|
@@ -2689,6 +2999,69 @@ var ThunderBridge = class {
|
|
|
2689
2999
|
async nameFor(paymentHash) {
|
|
2690
3000
|
return this.secret === null ? null : paymentNamedBy((await this.speaking()).publicKeyHex, paymentHash);
|
|
2691
3001
|
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Hold a socket open and answer what the gateway asks about this caller's own
|
|
3004
|
+
* payments, so a watch addressed to this caller settles without anybody
|
|
3005
|
+
* hosting a URL. Reconnects on its own until the returned function is called
|
|
3006
|
+
*/
|
|
3007
|
+
attend(options) {
|
|
3008
|
+
const base = this.baseUrl.replace(/^http/, "ws");
|
|
3009
|
+
const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
|
|
3010
|
+
let socket = null;
|
|
3011
|
+
let retry;
|
|
3012
|
+
let stopped = false;
|
|
3013
|
+
let attempt = 0;
|
|
3014
|
+
const again = () => {
|
|
3015
|
+
if (stopped || options.reconnect === false) {
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
retry = setTimeout(() => void connect(), backoffMs(firstDelay, attempt));
|
|
3019
|
+
};
|
|
3020
|
+
const answered3 = async (opened, raw) => {
|
|
3021
|
+
const asked = askFromWire(raw);
|
|
3022
|
+
if (asked === null) {
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
const preimage = await options.answer(asked.paymentHash);
|
|
3026
|
+
opened.send(
|
|
3027
|
+
JSON.stringify(
|
|
3028
|
+
preimage === null ? { ask: asked.ask, settled: false } : { ask: asked.ask, settled: true, preimage }
|
|
3029
|
+
)
|
|
3030
|
+
);
|
|
3031
|
+
};
|
|
3032
|
+
const connect = async () => {
|
|
3033
|
+
attempt += 1;
|
|
3034
|
+
let url;
|
|
3035
|
+
try {
|
|
3036
|
+
url = `${base}/ws/tickets/${await this.wsTicket({ agent: true })}`;
|
|
3037
|
+
} catch (refused) {
|
|
3038
|
+
options.onError?.(refused);
|
|
3039
|
+
again();
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (stopped) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
const opened = new WebSocket(url);
|
|
3046
|
+
socket = opened;
|
|
3047
|
+
opened.onopen = () => {
|
|
3048
|
+
attempt = 1;
|
|
3049
|
+
};
|
|
3050
|
+
opened.onmessage = (event) => {
|
|
3051
|
+
void answered3(opened, String(event.data)).catch(
|
|
3052
|
+
(refused) => options.onError?.(refused)
|
|
3053
|
+
);
|
|
3054
|
+
};
|
|
3055
|
+
opened.onerror = () => options.onError?.(new Error(`could not attend ${base}`));
|
|
3056
|
+
opened.onclose = again;
|
|
3057
|
+
};
|
|
3058
|
+
void connect();
|
|
3059
|
+
return () => {
|
|
3060
|
+
stopped = true;
|
|
3061
|
+
clearTimeout(retry);
|
|
3062
|
+
socket?.close();
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
2692
3065
|
/**
|
|
2693
3066
|
* Follow every payment made to one trigger, replayed from the recent ones on
|
|
2694
3067
|
* connect and then live, reconnecting on its own until the returned function
|
|
@@ -2765,7 +3138,7 @@ var ThunderBridge = class {
|
|
|
2765
3138
|
});
|
|
2766
3139
|
}
|
|
2767
3140
|
needsTicket(asked) {
|
|
2768
|
-
return asked === true || this.token !== null;
|
|
3141
|
+
return asked === true || this.token !== null || this.secret !== null;
|
|
2769
3142
|
}
|
|
2770
3143
|
async wsTicket(body) {
|
|
2771
3144
|
return encodeURIComponent((await this.mintedTicket(body)).ticket);
|
|
@@ -2802,8 +3175,26 @@ var ThunderBridge = class {
|
|
|
2802
3175
|
this.speaks ??= callerKey(this.secret ?? "");
|
|
2803
3176
|
return await this.speaks;
|
|
2804
3177
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
3178
|
+
handOut(payment, key) {
|
|
3179
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3180
|
+
for (const [hash2, earlier2] of this.handedOut) {
|
|
3181
|
+
if (earlier2.expiresAt <= now) {
|
|
3182
|
+
this.handedOut.delete(hash2);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
const hash = payment.paymentHash.toLowerCase();
|
|
3186
|
+
const earlier = this.handedOut.get(hash);
|
|
3187
|
+
if (earlier !== void 0 && (earlier.key === null || earlier.key !== key)) {
|
|
3188
|
+
throw new GatewayCheatError("invoice_reused", payment.id);
|
|
3189
|
+
}
|
|
3190
|
+
this.handedOut.set(hash, { key, expiresAt: payment.expiresAt });
|
|
3191
|
+
}
|
|
3192
|
+
proven(payment, held) {
|
|
3193
|
+
const other = held !== void 0 && (payment.id !== held.id || payment.paymentHash.toLowerCase() !== held.paymentHash.toLowerCase());
|
|
3194
|
+
if (other) {
|
|
3195
|
+
throw new GatewayCheatError("hash_mismatch", payment.id);
|
|
3196
|
+
}
|
|
3197
|
+
if (payment.status === "paid" && !carriesProof(payment)) {
|
|
2807
3198
|
throw new GatewayCheatError("preimage_mismatch", payment.id);
|
|
2808
3199
|
}
|
|
2809
3200
|
return payment;
|
|
@@ -2898,7 +3289,7 @@ var Gateways = class {
|
|
|
2898
3289
|
* saying so. When they all end without a payment, the first ending is the answer,
|
|
2899
3290
|
* and when they all fail, the first failure is thrown
|
|
2900
3291
|
*/
|
|
2901
|
-
async settled(
|
|
3292
|
+
async settled(held, options) {
|
|
2902
3293
|
const stopLosers = new AbortController();
|
|
2903
3294
|
const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
|
|
2904
3295
|
const unpaid = [];
|
|
@@ -2913,7 +3304,7 @@ var Gateways = class {
|
|
|
2913
3304
|
}
|
|
2914
3305
|
};
|
|
2915
3306
|
for (const gateway of this.each) {
|
|
2916
|
-
gateway.settled(
|
|
3307
|
+
gateway.settled(held, { ...options, signal }).then((watched) => {
|
|
2917
3308
|
if (watched.status === "paid") {
|
|
2918
3309
|
return resolve2(watched);
|
|
2919
3310
|
}
|