thunder-bridge 1.4.2 → 1.5.0
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 +135 -125
- package/dist/bank-B3mHISn1.d.cts +92 -0
- package/dist/bank-B3mHISn1.d.ts +92 -0
- package/dist/bank.cjs +141 -0
- package/dist/bank.d.cts +44 -0
- package/dist/bank.d.ts +44 -0
- package/dist/bank.js +114 -0
- package/dist/client-Cc5gtjGV.d.ts +733 -0
- package/dist/client-CzGZcByI.d.cts +733 -0
- package/dist/errors-0vbVoISA.d.ts +126 -0
- package/dist/errors-Dmh-Uoh8.d.cts +126 -0
- package/dist/index.cjs +1576 -1094
- package/dist/index.d.cts +11 -503
- package/dist/index.d.ts +11 -503
- package/dist/index.js +1556 -1052
- package/dist/{server.cjs → nwc.cjs} +261 -689
- package/dist/nwc.d.cts +120 -0
- package/dist/nwc.d.ts +120 -0
- package/dist/{server.js → nwc.js} +256 -667
- package/dist/price.cjs +240 -0
- package/dist/price.d.cts +17 -0
- package/dist/price.d.ts +17 -0
- package/dist/price.js +205 -0
- package/dist/qr-CF-YeXU1.d.cts +55 -0
- package/dist/qr-CF-YeXU1.d.ts +55 -0
- package/dist/qr.cjs +262 -0
- package/dist/qr.d.cts +1 -0
- package/dist/qr.d.ts +1 -0
- package/dist/qr.js +227 -0
- package/dist/types-BNPmVnA7.d.cts +252 -0
- package/dist/types-BNPmVnA7.d.ts +252 -0
- package/openapi.yaml +1 -1
- package/package.json +48 -9
- package/dist/rail-CqUfuYXJ.d.cts +0 -615
- package/dist/rail-CqUfuYXJ.d.ts +0 -615
- package/dist/server.d.cts +0 -145
- package/dist/server.d.ts +0 -145
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,69 +17,45 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
AmountError: () => AmountError,
|
|
23
34
|
GatewayCheatError: () => GatewayCheatError,
|
|
24
35
|
Gateways: () => Gateways,
|
|
25
|
-
IDEMPOTENCY_KEY_REUSED: () => IDEMPOTENCY_KEY_REUSED,
|
|
26
36
|
IdempotencyConflictError: () => IdempotencyConflictError,
|
|
27
|
-
NO_WALLET_AVAILABLE: () => NO_WALLET_AVAILABLE,
|
|
28
37
|
NoWalletAvailableError: () => NoWalletAvailableError,
|
|
29
|
-
PAYMENT_ALREADY_WATCHED: () => PAYMENT_ALREADY_WATCHED,
|
|
30
38
|
ProblemError: () => ProblemError,
|
|
31
|
-
REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
|
|
32
39
|
ThunderBridge: () => ThunderBridge,
|
|
33
40
|
UnverifiedRecipientError: () => UnverifiedRecipientError,
|
|
34
41
|
WrapRefusedError: () => WrapRefusedError,
|
|
35
42
|
answerVerifyChallenge: () => answerVerifyChallenge,
|
|
36
|
-
|
|
37
|
-
answerWebhookChallenge: () => answerWebhookChallenge,
|
|
38
|
-
answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
|
|
39
|
-
bankRail: () => bankRail,
|
|
40
|
-
bankTransfer: () => bankTransfer,
|
|
41
|
-
bankVerifyEndpoint: () => bankVerifyEndpoint,
|
|
42
|
-
bitstamp: () => bitstamp,
|
|
43
|
-
coinbase: () => coinbase,
|
|
44
|
-
coinmate: () => coinmate,
|
|
43
|
+
carriesProof: () => carriesProof,
|
|
45
44
|
decodeInvoice: () => decodeInvoice,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
isProblemType: () => isProblemType,
|
|
50
|
-
isProvablyPaid: () => isProvablyPaid,
|
|
51
|
-
isProvablySettled: () => isProvablySettled,
|
|
52
|
-
kraken: () => kraken,
|
|
53
|
-
lightningRail: () => lightningRail,
|
|
54
|
-
lnurlToDataUrl: () => lnurlToDataUrl,
|
|
55
|
-
lnurlToSvg: () => lnurlToSvg,
|
|
56
|
-
medianOf: () => medianOf,
|
|
57
|
-
minorScaleOf: () => minorScaleOf,
|
|
58
|
-
minorUnitsOf: () => minorUnitsOf,
|
|
59
|
-
msatFor: () => msatFor,
|
|
60
|
-
parseSettlement: () => parseSettlement,
|
|
61
|
-
parseSettlementRequest: () => parseSettlementRequest,
|
|
62
|
-
parseWatchedWebhook: () => parseWatchedWebhook,
|
|
63
|
-
parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
|
|
64
|
-
parseWebhook: () => parseWebhook,
|
|
65
|
-
parseWebhookRequest: () => parseWebhookRequest,
|
|
45
|
+
fiat: () => fiat,
|
|
46
|
+
invoiceFrom: () => invoiceFrom,
|
|
47
|
+
msat: () => msat,
|
|
66
48
|
preimageMatchesHash: () => preimageMatchesHash,
|
|
67
49
|
proveOrigin: () => proveOrigin,
|
|
68
50
|
proveSettlement: () => proveSettlement,
|
|
69
51
|
proveWrapped: () => proveWrapped,
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
relayedVerifyUrl: () => relayedVerifyUrl,
|
|
53
|
+
sats: () => sats,
|
|
72
54
|
seal: () => seal,
|
|
73
|
-
spdToDataUrl: () => spdToDataUrl,
|
|
74
|
-
spdToSvg: () => spdToSvg,
|
|
75
|
-
toLnurl: () => toLnurl,
|
|
76
55
|
unseal: () => unseal,
|
|
77
|
-
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
78
56
|
wrapFeeCeiling: () => wrapFeeCeiling
|
|
79
57
|
});
|
|
80
|
-
module.exports = __toCommonJS(
|
|
58
|
+
module.exports = __toCommonJS(src_exports);
|
|
81
59
|
|
|
82
60
|
// ../core/bytes.ts
|
|
83
61
|
function bytesToHex(bytes) {
|
|
@@ -94,8 +72,8 @@ function hexToBytes(hex) {
|
|
|
94
72
|
}
|
|
95
73
|
return bytes;
|
|
96
74
|
}
|
|
97
|
-
function isHex(
|
|
98
|
-
return /^[0-9a-f]+$/i.test(
|
|
75
|
+
function isHex(text3) {
|
|
76
|
+
return /^[0-9a-f]+$/i.test(text3) && text3.length % 2 === 0;
|
|
99
77
|
}
|
|
100
78
|
|
|
101
79
|
// ../core/sha256.ts
|
|
@@ -189,8 +167,8 @@ function sha256(bytes) {
|
|
|
189
167
|
}
|
|
190
168
|
return asBytes(state);
|
|
191
169
|
}
|
|
192
|
-
function sha256Hex(
|
|
193
|
-
return bytesToHex(sha256(new TextEncoder().encode(
|
|
170
|
+
function sha256Hex(text3) {
|
|
171
|
+
return bytesToHex(sha256(new TextEncoder().encode(text3)));
|
|
194
172
|
}
|
|
195
173
|
function withPadding(bytes) {
|
|
196
174
|
const padded = new Uint8Array(
|
|
@@ -377,158 +355,6 @@ function wordsToBytes(words) {
|
|
|
377
355
|
return Uint8Array.from(bytes);
|
|
378
356
|
}
|
|
379
357
|
|
|
380
|
-
// ../core/url.ts
|
|
381
|
-
var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
|
|
382
|
-
function publicHttps(raw) {
|
|
383
|
-
let url;
|
|
384
|
-
try {
|
|
385
|
-
url = new URL(raw);
|
|
386
|
-
} catch {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
|
|
390
|
-
}
|
|
391
|
-
function sameOrigin(one, other) {
|
|
392
|
-
try {
|
|
393
|
-
return new URL(one).origin === new URL(other).origin;
|
|
394
|
-
} catch {
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function publicAddress(literal) {
|
|
399
|
-
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
400
|
-
}
|
|
401
|
-
function hostIsPublic(host) {
|
|
402
|
-
if (host.startsWith("[")) {
|
|
403
|
-
return publicAddress(host.slice(1, -1));
|
|
404
|
-
}
|
|
405
|
-
const name = host.replace(/\.$/, "");
|
|
406
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
|
|
407
|
-
return publicAddress(name);
|
|
408
|
-
}
|
|
409
|
-
const labels = name.split(".");
|
|
410
|
-
if (labels.length < 2 || labels.some((label) => label === "")) {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
414
|
-
}
|
|
415
|
-
function ipv4IsGlobal(literal) {
|
|
416
|
-
const parts = literal.split(".").map(Number);
|
|
417
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
const [a, b] = parts;
|
|
421
|
-
if (a === 0 || a === 10 || a === 127 || a >= 240) {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
if (a === 100 && b >= 64 && b < 128) {
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
if (a === 169 && b === 254) {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
if (a === 172 && b >= 16 && b < 32) {
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
if (a === 192 && b === 168) {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
function ipv6IsGlobal(literal) {
|
|
439
|
-
const groups = ipv6Groups(literal.toLowerCase());
|
|
440
|
-
if (groups.length !== 8 || groups.some(Number.isNaN)) {
|
|
441
|
-
return false;
|
|
442
|
-
}
|
|
443
|
-
const leading = groups.slice(0, 5).every((group) => group === 0);
|
|
444
|
-
if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
if (leading && groups[5] === 65535) {
|
|
448
|
-
const [high, low] = [groups[6], groups[7]];
|
|
449
|
-
return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
450
|
-
}
|
|
451
|
-
const first = groups[0];
|
|
452
|
-
if ((first & 65024) === 64512) {
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
if ((first & 65472) === 65152) {
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
function ipv6Groups(address) {
|
|
461
|
-
const [head = "", tail] = address.split("::");
|
|
462
|
-
const left = head.split(":").filter((group) => group !== "");
|
|
463
|
-
const right = (tail ?? "").split(":").filter((group) => group !== "");
|
|
464
|
-
const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
|
|
465
|
-
return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ../core/lnurl.ts
|
|
469
|
-
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
470
|
-
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
471
|
-
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
472
|
-
var LNURL_HRP = "lnurl";
|
|
473
|
-
function toLnurl(endpoint) {
|
|
474
|
-
const scheme = parsedScheme(endpoint);
|
|
475
|
-
if (scheme !== "https:" && scheme !== "http:") {
|
|
476
|
-
throw new Error(`${endpoint} is not an http or https URL`);
|
|
477
|
-
}
|
|
478
|
-
const words = toWords(new TextEncoder().encode(endpoint));
|
|
479
|
-
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
480
|
-
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
481
|
-
}
|
|
482
|
-
function parsedScheme(endpoint) {
|
|
483
|
-
try {
|
|
484
|
-
return new URL(endpoint).protocol;
|
|
485
|
-
} catch {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
function toWords(bytes) {
|
|
490
|
-
const words = [];
|
|
491
|
-
let accumulator = 0;
|
|
492
|
-
let bits = 0;
|
|
493
|
-
for (const byte of bytes) {
|
|
494
|
-
accumulator = accumulator << 8 | byte;
|
|
495
|
-
bits += 8;
|
|
496
|
-
while (bits >= 5) {
|
|
497
|
-
bits -= 5;
|
|
498
|
-
words.push(accumulator >> bits & 31);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (bits > 0) {
|
|
502
|
-
words.push(accumulator << 5 - bits & 31);
|
|
503
|
-
}
|
|
504
|
-
return words;
|
|
505
|
-
}
|
|
506
|
-
function checksumWords(hrp, words) {
|
|
507
|
-
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
508
|
-
const values = [
|
|
509
|
-
...expanded.map((code) => code >> 5),
|
|
510
|
-
0,
|
|
511
|
-
...expanded.map((code) => code & 31),
|
|
512
|
-
...words,
|
|
513
|
-
...CHECKSUM_SLOTS.map(() => 0)
|
|
514
|
-
];
|
|
515
|
-
const polymod = bech32Polymod(values) ^ 1;
|
|
516
|
-
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
517
|
-
}
|
|
518
|
-
function bech32Polymod(values) {
|
|
519
|
-
let checksum = 1;
|
|
520
|
-
for (const value of values) {
|
|
521
|
-
const top = checksum >> 25;
|
|
522
|
-
checksum = (checksum & 33554431) << 5 ^ value;
|
|
523
|
-
for (let bit = 0; bit < 5; bit++) {
|
|
524
|
-
if (top >> bit & 1) {
|
|
525
|
-
checksum ^= BECH32_GENERATOR[bit];
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return checksum;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
358
|
// ../core/sealed.ts
|
|
533
359
|
var VERSION = "v1";
|
|
534
360
|
var IV_BYTES = 12;
|
|
@@ -594,12 +420,12 @@ function toBase64Url(bytes) {
|
|
|
594
420
|
}
|
|
595
421
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
596
422
|
}
|
|
597
|
-
function fromBase64Url(
|
|
598
|
-
if (
|
|
423
|
+
function fromBase64Url(text3) {
|
|
424
|
+
if (text3.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text3)) {
|
|
599
425
|
return null;
|
|
600
426
|
}
|
|
601
427
|
try {
|
|
602
|
-
const binary = atob(
|
|
428
|
+
const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
|
|
603
429
|
const bytes = new Uint8Array(binary.length);
|
|
604
430
|
for (let at = 0; at < bytes.length; at++) {
|
|
605
431
|
bytes[at] = binary.charCodeAt(at);
|
|
@@ -610,32 +436,109 @@ function fromBase64Url(text2) {
|
|
|
610
436
|
}
|
|
611
437
|
}
|
|
612
438
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
|
|
625
|
-
}
|
|
626
|
-
function equalInConstantTime(one, other) {
|
|
627
|
-
if (one.length !== other.length) {
|
|
628
|
-
return false;
|
|
439
|
+
// src/errors.ts
|
|
440
|
+
var AmountError = class extends Error {
|
|
441
|
+
/**
|
|
442
|
+
* Whether a failure is one of these, without asking whether it is this exact
|
|
443
|
+
* class. Every entry point carries its own copy of the class, so a price
|
|
444
|
+
* refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
|
|
445
|
+
* imported from `thunder-bridge`. The name and the code are the same in every
|
|
446
|
+
* copy, so this holds where `instanceof` does not
|
|
447
|
+
*/
|
|
448
|
+
static is(failure) {
|
|
449
|
+
return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
|
|
629
450
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
451
|
+
code;
|
|
452
|
+
constructor(code, detail) {
|
|
453
|
+
super(`${code}: ${detail}`);
|
|
454
|
+
this.name = "AmountError";
|
|
455
|
+
this.code = code;
|
|
633
456
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
457
|
+
};
|
|
458
|
+
var GatewayCheatError = class extends Error {
|
|
459
|
+
code;
|
|
460
|
+
paymentId;
|
|
461
|
+
constructor(code, paymentId) {
|
|
462
|
+
super(`gateway verification failed: ${code}`);
|
|
463
|
+
this.name = "GatewayCheatError";
|
|
464
|
+
this.code = code;
|
|
465
|
+
this.paymentId = paymentId;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var WrapRefusedError = class extends Error {
|
|
469
|
+
code;
|
|
470
|
+
constructor(code, detail) {
|
|
471
|
+
super(`wrapped invoice refused: ${code}, ${detail}`);
|
|
472
|
+
this.name = "WrapRefusedError";
|
|
473
|
+
this.code = code;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
var UnverifiedRecipientError = class extends Error {
|
|
477
|
+
lnAddress;
|
|
478
|
+
paymentId;
|
|
479
|
+
constructor(lnAddress, paymentId, cause) {
|
|
480
|
+
super(`could not reach ${lnAddress} to verify the invoice: ${String(cause)}`);
|
|
481
|
+
this.name = "UnverifiedRecipientError";
|
|
482
|
+
this.lnAddress = lnAddress;
|
|
483
|
+
this.paymentId = paymentId;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var ProblemError = class extends Error {
|
|
487
|
+
static NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
488
|
+
static REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
489
|
+
static IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
|
|
490
|
+
static PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
|
|
491
|
+
static INVALID_REQUEST = "urn:problem-type:thunder-bridge:invalid-request";
|
|
492
|
+
static CALLER_UNKNOWN = "urn:problem-type:thunder-bridge:caller-unknown";
|
|
493
|
+
static VERIFY_HOST_REFUSED = "urn:problem-type:thunder-bridge:verify-host-refused";
|
|
494
|
+
static VERIFY_UNCONFIRMED = "urn:problem-type:thunder-bridge:verify-unconfirmed";
|
|
495
|
+
static VERIFY_UNCONSENTED = "urn:problem-type:thunder-bridge:verify-unconsented";
|
|
496
|
+
static WEBHOOK_UNCONFIRMED = "urn:problem-type:thunder-bridge:webhook-unconfirmed";
|
|
497
|
+
static TOO_MANY_PENDING = "urn:problem-type:thunder-bridge:too-many-pending";
|
|
498
|
+
/**
|
|
499
|
+
* Whether a problem carries this type. Branch on the type, never on the prose,
|
|
500
|
+
* and reach for `instanceof` first: every type this SDK gives a class to has one
|
|
501
|
+
*/
|
|
502
|
+
static is(problem, type) {
|
|
503
|
+
return problem.type === type;
|
|
504
|
+
}
|
|
505
|
+
type;
|
|
506
|
+
title;
|
|
507
|
+
status;
|
|
508
|
+
detail;
|
|
509
|
+
constructor(problem) {
|
|
510
|
+
const title = problem.title ?? "Request failed";
|
|
511
|
+
super(problem.detail ? `${title}: ${problem.detail}` : title);
|
|
512
|
+
this.name = "ProblemError";
|
|
513
|
+
this.type = problem.type ?? "about:blank";
|
|
514
|
+
this.title = title;
|
|
515
|
+
this.status = problem.status ?? 0;
|
|
516
|
+
this.detail = problem.detail ?? null;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
function isProblemType(problem, type) {
|
|
520
|
+
return problem.type === type;
|
|
638
521
|
}
|
|
522
|
+
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
523
|
+
var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
524
|
+
var IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
|
|
525
|
+
var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
|
|
526
|
+
var IdempotencyConflictError = class extends ProblemError {
|
|
527
|
+
conflict;
|
|
528
|
+
constructor(problem, conflict) {
|
|
529
|
+
super(problem);
|
|
530
|
+
this.name = "IdempotencyConflictError";
|
|
531
|
+
this.conflict = conflict;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
var NoWalletAvailableError = class extends ProblemError {
|
|
535
|
+
wallets;
|
|
536
|
+
constructor(problem, wallets) {
|
|
537
|
+
super({ ...problem, type: NO_WALLET_AVAILABLE });
|
|
538
|
+
this.name = "NoWalletAvailableError";
|
|
539
|
+
this.wallets = wallets;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
639
542
|
|
|
640
543
|
// src/currency.ts
|
|
641
544
|
var ISO_4217_MINOR_UNITS = {
|
|
@@ -669,72 +572,770 @@ var ISO_4217_MINOR_UNITS = {
|
|
|
669
572
|
function minorUnitsOf(currency) {
|
|
670
573
|
const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
|
|
671
574
|
if (digits === void 0) {
|
|
672
|
-
throw new
|
|
575
|
+
throw new AmountError(
|
|
576
|
+
"unknown-currency",
|
|
673
577
|
`${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
|
|
674
578
|
);
|
|
675
579
|
}
|
|
676
|
-
return digits;
|
|
580
|
+
return digits;
|
|
581
|
+
}
|
|
582
|
+
function minorScaleOf(currency) {
|
|
583
|
+
return 10 ** minorUnitsOf(currency);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/price.ts
|
|
587
|
+
var MSAT_PER_BTC2 = 1e11;
|
|
588
|
+
var BASIS_POINTS = 1e4;
|
|
589
|
+
var DEFAULT_HOLD_SECS = 60;
|
|
590
|
+
var DEFAULT_MIN_VENUES = 2;
|
|
591
|
+
var DEFAULT_MAX_SPREAD_BPS = 200;
|
|
592
|
+
var MILLIS = 1e3;
|
|
593
|
+
function coinbase(baseUrl = "https://api.coinbase.com") {
|
|
594
|
+
return async (currency) => {
|
|
595
|
+
const asked = currency.toUpperCase();
|
|
596
|
+
const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
|
|
597
|
+
const quoted = answer.data;
|
|
598
|
+
if (quoted?.currency !== asked) {
|
|
599
|
+
throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
|
|
600
|
+
}
|
|
601
|
+
return asMinor(quoted.amount, "coinbase", asked);
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function kraken(baseUrl = "https://api.kraken.com") {
|
|
605
|
+
return async (currency) => {
|
|
606
|
+
const asked = currency.toUpperCase();
|
|
607
|
+
const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
|
|
608
|
+
if (answer.error?.length) {
|
|
609
|
+
throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
|
|
610
|
+
}
|
|
611
|
+
const pairs = Object.values(answer.result ?? {});
|
|
612
|
+
if (pairs.length !== 1) {
|
|
613
|
+
throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
|
|
614
|
+
}
|
|
615
|
+
return asMinor(pairs[0]?.c?.[0], "kraken", asked);
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function bitstamp(baseUrl = "https://www.bitstamp.net") {
|
|
619
|
+
return async (currency) => {
|
|
620
|
+
const pair = `btc${currency.toLowerCase()}`;
|
|
621
|
+
const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
|
|
622
|
+
if (Array.isArray(answer)) {
|
|
623
|
+
throw new Error(`bitstamp has no ${pair} pair`);
|
|
624
|
+
}
|
|
625
|
+
return asMinor(answer.last, "bitstamp", currency);
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function coinmate(baseUrl = "https://coinmate.io") {
|
|
629
|
+
return async (currency) => {
|
|
630
|
+
const pair = `BTC_${currency.toUpperCase()}`;
|
|
631
|
+
const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
|
|
632
|
+
if (answer.error !== false) {
|
|
633
|
+
throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
|
|
634
|
+
}
|
|
635
|
+
return asMinor(answer.data?.last, "coinmate", currency);
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
|
|
639
|
+
const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
|
|
640
|
+
const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
|
|
641
|
+
const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
|
|
642
|
+
const held = /* @__PURE__ */ new Map();
|
|
643
|
+
return async (currency) => {
|
|
644
|
+
const asked = currency.toUpperCase();
|
|
645
|
+
const remembered = held.get(asked);
|
|
646
|
+
if (remembered && Date.now() - remembered.at < holdFor) {
|
|
647
|
+
return remembered.price;
|
|
648
|
+
}
|
|
649
|
+
const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
|
|
650
|
+
const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
|
|
651
|
+
if (quoted.length < minVenues) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
`${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const spread2 = spreadBpsOf(quoted);
|
|
657
|
+
if (spread2 > maxSpread) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`venues disagree on ${asked} by ${Math.round(spread2)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const price = middleOf(quoted);
|
|
663
|
+
held.set(asked, { at: Date.now(), price });
|
|
664
|
+
return price;
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
|
|
668
|
+
if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
|
|
669
|
+
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
670
|
+
}
|
|
671
|
+
if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
|
|
672
|
+
throw new Error(`${priceMinorPerBtc} is not a usable price`);
|
|
673
|
+
}
|
|
674
|
+
const spreadBps = options.spreadBps ?? 0;
|
|
675
|
+
if (!Number.isInteger(spreadBps) || spreadBps < 0) {
|
|
676
|
+
throw new Error("spreadBps must be a whole number of basis points, none or more");
|
|
677
|
+
}
|
|
678
|
+
const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
|
|
679
|
+
const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
|
|
680
|
+
const msat2 = (wanted + per - 1n) / per;
|
|
681
|
+
if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
682
|
+
throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
|
|
683
|
+
}
|
|
684
|
+
return Number(msat2);
|
|
685
|
+
}
|
|
686
|
+
async function asJson(url, venue) {
|
|
687
|
+
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
688
|
+
if (!answer.ok) {
|
|
689
|
+
throw new Error(`${venue} answered ${answer.status}`);
|
|
690
|
+
}
|
|
691
|
+
return answer.json();
|
|
692
|
+
}
|
|
693
|
+
function asMinor(quoted, venue, currency) {
|
|
694
|
+
const major2 = Number(quoted);
|
|
695
|
+
if (!Number.isFinite(major2) || major2 <= 0) {
|
|
696
|
+
throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
|
|
697
|
+
}
|
|
698
|
+
return Math.round(major2 * minorScaleOf(currency));
|
|
699
|
+
}
|
|
700
|
+
function spreadBpsOf(sorted) {
|
|
701
|
+
const lowest = sorted[0] ?? 0;
|
|
702
|
+
const highest = sorted[sorted.length - 1] ?? 0;
|
|
703
|
+
return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
|
|
704
|
+
}
|
|
705
|
+
function middleOf(sorted) {
|
|
706
|
+
const middle = Math.floor(sorted.length / 2);
|
|
707
|
+
if (sorted.length % 2 === 1) {
|
|
708
|
+
return sorted[middle] ?? 0;
|
|
709
|
+
}
|
|
710
|
+
return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
|
|
711
|
+
}
|
|
712
|
+
function refusals(answers) {
|
|
713
|
+
return answers.filter((answer) => answer.status === "rejected").map(
|
|
714
|
+
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
715
|
+
).join("; ");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/amount.ts
|
|
719
|
+
var MSAT_PER_SAT = 1e3;
|
|
720
|
+
var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
|
|
721
|
+
function sats(whole) {
|
|
722
|
+
if (!Number.isInteger(whole) || whole < 1 || whole > MAX_SATS) {
|
|
723
|
+
throw new AmountError("not-whole-satoshi", `${whole} is not a payable count of satoshi`);
|
|
724
|
+
}
|
|
725
|
+
return whole * MSAT_PER_SAT;
|
|
726
|
+
}
|
|
727
|
+
function msat(exact) {
|
|
728
|
+
if (!Number.isSafeInteger(exact) || exact < 1) {
|
|
729
|
+
throw new AmountError(
|
|
730
|
+
"not-whole-millisatoshi",
|
|
731
|
+
`${exact} is not a payable count of millisatoshi`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return exact;
|
|
735
|
+
}
|
|
736
|
+
function fiat(major2, currency, options) {
|
|
737
|
+
const amountMinor = minorFrom(major2, currency);
|
|
738
|
+
const rate = options?.rate ?? medianOf();
|
|
739
|
+
return async () => {
|
|
740
|
+
const wanted = msatFor(amountMinor, await rate(currency), { spreadBps: options?.spreadBps });
|
|
741
|
+
return sats(Math.ceil(wanted / MSAT_PER_SAT));
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function amountNow(amount) {
|
|
745
|
+
return typeof amount === "function" ? msat(await amount()) : msat(amount);
|
|
746
|
+
}
|
|
747
|
+
function minorFrom(major2, currency) {
|
|
748
|
+
const digits = minorUnitsOf(currency);
|
|
749
|
+
if (typeof major2 === "number") {
|
|
750
|
+
return payable(Math.round(major2 * minorScaleOf(currency)), major2, currency);
|
|
751
|
+
}
|
|
752
|
+
const parsed = /^(\d+)(?:[.,](\d+))?$/.exec(major2.trim());
|
|
753
|
+
if (parsed === null) {
|
|
754
|
+
throw new AmountError(
|
|
755
|
+
"not-a-decimal",
|
|
756
|
+
`${major2} is not a decimal ${currency.toUpperCase()} amount`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const fraction = parsed[2] ?? "";
|
|
760
|
+
if (fraction.length > digits) {
|
|
761
|
+
throw new AmountError(
|
|
762
|
+
"too-precise",
|
|
763
|
+
`${major2} carries more decimals than ${currency.toUpperCase()} has, which is ${digits}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return payable(Number(`${parsed[1]}${fraction.padEnd(digits, "0")}`), major2, currency);
|
|
767
|
+
}
|
|
768
|
+
function payable(minor, major2, currency) {
|
|
769
|
+
if (!Number.isSafeInteger(minor) || minor < 1) {
|
|
770
|
+
throw new AmountError(
|
|
771
|
+
"not-a-decimal",
|
|
772
|
+
`${major2} is not a ${currency.toUpperCase()} price this can charge`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
return minor;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ../core/ed25519.ts
|
|
779
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
780
|
+
var SEED_BYTES = 32;
|
|
781
|
+
var SIGNATURE_BYTES = 64;
|
|
782
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
783
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
784
|
+
async function signingKeyFromSeed(seed) {
|
|
785
|
+
if (seed.length !== SEED_BYTES) {
|
|
786
|
+
throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
|
|
787
|
+
}
|
|
788
|
+
const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
|
|
789
|
+
wrapped.set(PKCS8_HEADER);
|
|
790
|
+
wrapped.set(seed, PKCS8_HEADER.length);
|
|
791
|
+
const key = await crypto.subtle.importKey("pkcs8", asBuffer(wrapped), ALGORITHM, true, ["sign"]);
|
|
792
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
793
|
+
return {
|
|
794
|
+
publicKeyHex: bytesToHex(fromBase64Url2(jwk.x ?? "")),
|
|
795
|
+
sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer(payload))))
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
799
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const key = await crypto.subtle.importKey(
|
|
807
|
+
"raw",
|
|
808
|
+
asBuffer(hexToBytes(publicKeyHex)),
|
|
809
|
+
ALGORITHM,
|
|
810
|
+
false,
|
|
811
|
+
["verify"]
|
|
812
|
+
);
|
|
813
|
+
return await crypto.subtle.verify(
|
|
814
|
+
ALGORITHM,
|
|
815
|
+
key,
|
|
816
|
+
asBuffer(hexToBytes(signatureHex)),
|
|
817
|
+
asBuffer(payload)
|
|
818
|
+
);
|
|
819
|
+
} catch {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function fromBase64Url2(text3) {
|
|
824
|
+
const padded = text3.replace(/-/g, "+").replace(/_/g, "/");
|
|
825
|
+
const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
826
|
+
const bytes = new Uint8Array(raw.length);
|
|
827
|
+
for (let at = 0; at < raw.length; at++) {
|
|
828
|
+
bytes[at] = raw.charCodeAt(at);
|
|
829
|
+
}
|
|
830
|
+
return bytes;
|
|
831
|
+
}
|
|
832
|
+
function asBuffer(bytes) {
|
|
833
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ../core/hmac.ts
|
|
837
|
+
async function hmacHex(secret, payload) {
|
|
838
|
+
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
839
|
+
const key = await crypto.subtle.importKey(
|
|
840
|
+
"raw",
|
|
841
|
+
asBuffer2(raw),
|
|
842
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
843
|
+
false,
|
|
844
|
+
["sign"]
|
|
845
|
+
);
|
|
846
|
+
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
847
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
|
|
848
|
+
}
|
|
849
|
+
function equalInConstantTime(one, other) {
|
|
850
|
+
if (one.length !== other.length) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
let difference = 0;
|
|
854
|
+
for (let at = 0; at < one.length; at++) {
|
|
855
|
+
difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
856
|
+
}
|
|
857
|
+
return difference === 0;
|
|
858
|
+
}
|
|
859
|
+
function asBuffer2(bytes) {
|
|
860
|
+
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ../core/caller.ts
|
|
864
|
+
var KEY_HEADER = "x-client-key";
|
|
865
|
+
var SIGNATURE_HEADER = "x-signature";
|
|
866
|
+
var TIMESTAMP_HEADER = "x-timestamp";
|
|
867
|
+
var PREFIX = "ed25519=";
|
|
868
|
+
var SIGNING_LABEL = "request-signing";
|
|
869
|
+
async function callerKey(secret) {
|
|
870
|
+
return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
|
|
871
|
+
}
|
|
872
|
+
function paymentNamedBy(caller, paymentHash) {
|
|
873
|
+
return sha256Hex(`${caller}.payment-id.${paymentHash}`);
|
|
874
|
+
}
|
|
875
|
+
async function signedAs(key, method, path, body) {
|
|
876
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
877
|
+
return {
|
|
878
|
+
[KEY_HEADER]: key.publicKeyHex,
|
|
879
|
+
[TIMESTAMP_HEADER]: timestamp,
|
|
880
|
+
[SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function spoken(timestamp, method, path, body) {
|
|
884
|
+
return new TextEncoder().encode(
|
|
885
|
+
`${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/charge.ts
|
|
890
|
+
async function priced(charge) {
|
|
891
|
+
const paidTo = typeof charge.paidTo === "string" ? [charge.paidTo] : [...charge.paidTo];
|
|
892
|
+
if (paidTo.length === 0) {
|
|
893
|
+
throw new Error("name at least one lightning address to pay");
|
|
894
|
+
}
|
|
895
|
+
return { paidTo, amountMsat: await amountNow(charge.amount) };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ../core/url.ts
|
|
899
|
+
var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
|
|
900
|
+
function publicHttps(raw) {
|
|
901
|
+
let url;
|
|
902
|
+
try {
|
|
903
|
+
url = new URL(raw);
|
|
904
|
+
} catch {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
|
|
908
|
+
}
|
|
909
|
+
function sameOrigin(one, other) {
|
|
910
|
+
try {
|
|
911
|
+
return new URL(one).origin === new URL(other).origin;
|
|
912
|
+
} catch {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function publicAddress(literal) {
|
|
917
|
+
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
918
|
+
}
|
|
919
|
+
function hostIsPublic(host) {
|
|
920
|
+
if (host.startsWith("[")) {
|
|
921
|
+
return publicAddress(host.slice(1, -1));
|
|
922
|
+
}
|
|
923
|
+
const name = host.replace(/\.$/, "");
|
|
924
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
|
|
925
|
+
return publicAddress(name);
|
|
926
|
+
}
|
|
927
|
+
const labels = name.split(".");
|
|
928
|
+
if (labels.length < 2 || labels.some((label) => label === "")) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
932
|
+
}
|
|
933
|
+
function ipv4IsGlobal(literal) {
|
|
934
|
+
const parts = literal.split(".").map(Number);
|
|
935
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
const [a, b] = parts;
|
|
939
|
+
if (a === 0 || a === 10 || a === 127 || a >= 240) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
if (a === 100 && b >= 64 && b < 128) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (a === 169 && b === 254) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
if (a === 172 && b >= 16 && b < 32) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (a === 192 && b === 168) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
function ipv6IsGlobal(literal) {
|
|
957
|
+
const groups = ipv6Groups(literal.toLowerCase());
|
|
958
|
+
if (groups.length !== 8 || groups.some(Number.isNaN)) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
const leading = groups.slice(0, 5).every((group) => group === 0);
|
|
962
|
+
if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
if (leading && groups[5] === 65535) {
|
|
966
|
+
const [high, low] = [groups[6], groups[7]];
|
|
967
|
+
return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
968
|
+
}
|
|
969
|
+
const first = groups[0];
|
|
970
|
+
if ((first & 65024) === 64512) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
if ((first & 65472) === 65152) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
function ipv6Groups(address) {
|
|
979
|
+
const [head = "", tail] = address.split("::");
|
|
980
|
+
const left = head.split(":").filter((group) => group !== "");
|
|
981
|
+
const right = (tail ?? "").split(":").filter((group) => group !== "");
|
|
982
|
+
const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
|
|
983
|
+
return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ../core/outbound.ts
|
|
987
|
+
var HTTP_TIMEOUT_MS = 15e3;
|
|
988
|
+
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
989
|
+
var HOPS_FOLLOWED = 2;
|
|
990
|
+
var REDIRECTS = [301, 302, 303, 307, 308];
|
|
991
|
+
var KEEPS_THE_METHOD = [307, 308];
|
|
992
|
+
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
993
|
+
var BODY_LIMIT_BYTES = 262144;
|
|
994
|
+
async function ask(send, url, sent = {}) {
|
|
995
|
+
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
|
|
996
|
+
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
997
|
+
let target = url;
|
|
998
|
+
let carried = sent;
|
|
999
|
+
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
1000
|
+
const at = await addressesToReach(target);
|
|
1001
|
+
const response = await send(target, carried, signal, at);
|
|
1002
|
+
if (!REDIRECTS.includes(response.status)) {
|
|
1003
|
+
return await answerOf(response);
|
|
1004
|
+
}
|
|
1005
|
+
const location = response.headers.get("location");
|
|
1006
|
+
if (location === null) {
|
|
1007
|
+
throw new Error(`${target} answered ${response.status} without saying where to`);
|
|
1008
|
+
}
|
|
1009
|
+
await response.body?.cancel();
|
|
1010
|
+
const next = new URL(location, target).toString();
|
|
1011
|
+
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
1012
|
+
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
1013
|
+
target = next;
|
|
1014
|
+
}
|
|
1015
|
+
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
1016
|
+
}
|
|
1017
|
+
async function addressesToReach(url) {
|
|
1018
|
+
if (!publicHttps(url)) {
|
|
1019
|
+
throw new Error(`${url} is not a public https URL`);
|
|
1020
|
+
}
|
|
1021
|
+
const found = await resolved(url);
|
|
1022
|
+
if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
|
|
1023
|
+
throw new Error(`${url} resolves to an address we do not reach`);
|
|
1024
|
+
}
|
|
1025
|
+
return found;
|
|
1026
|
+
}
|
|
1027
|
+
async function resolved(url) {
|
|
1028
|
+
const { lookup } = await import("dns/promises");
|
|
1029
|
+
const { setTimeout: sleep } = await import("timers/promises");
|
|
1030
|
+
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
1031
|
+
const found = await Promise.race([
|
|
1032
|
+
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
1033
|
+
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
1034
|
+
]);
|
|
1035
|
+
return found ?? [];
|
|
1036
|
+
}
|
|
1037
|
+
async function answerOf(response) {
|
|
1038
|
+
const { status, ok, headers } = response;
|
|
1039
|
+
const reader = response.body?.getReader();
|
|
1040
|
+
if (!reader) {
|
|
1041
|
+
return { status, ok, body: "", truncated: false, headers };
|
|
1042
|
+
}
|
|
1043
|
+
const read = [];
|
|
1044
|
+
let bytes = 0;
|
|
1045
|
+
while (bytes <= BODY_LIMIT_BYTES) {
|
|
1046
|
+
const { done, value } = await reader.read();
|
|
1047
|
+
if (done) {
|
|
1048
|
+
return { status, ok, body: text(read), truncated: false, headers };
|
|
1049
|
+
}
|
|
1050
|
+
read.push(value);
|
|
1051
|
+
bytes += value.length;
|
|
1052
|
+
}
|
|
1053
|
+
await reader.cancel();
|
|
1054
|
+
return { status, ok, body: text(read), truncated: true, headers };
|
|
1055
|
+
}
|
|
1056
|
+
function withoutBody(sent) {
|
|
1057
|
+
const headers = { ...sent.headers };
|
|
1058
|
+
delete headers["content-type"];
|
|
1059
|
+
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1060
|
+
}
|
|
1061
|
+
function withoutCredentials(sent) {
|
|
1062
|
+
const headers = { ...sent.headers };
|
|
1063
|
+
for (const named of CREDENTIALS) {
|
|
1064
|
+
delete headers[named];
|
|
1065
|
+
}
|
|
1066
|
+
return { ...sent, headers };
|
|
1067
|
+
}
|
|
1068
|
+
function text(chunks) {
|
|
1069
|
+
const decoder = new TextDecoder();
|
|
1070
|
+
let decoded2 = "";
|
|
1071
|
+
for (const chunk of chunks) {
|
|
1072
|
+
decoded2 += decoder.decode(chunk, { stream: true });
|
|
1073
|
+
}
|
|
1074
|
+
return decoded2 + decoder.decode();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ../core/refusal.ts
|
|
1078
|
+
var WalletRefused = class extends Error {
|
|
1079
|
+
reason;
|
|
1080
|
+
constructor(reason, message) {
|
|
1081
|
+
super(message);
|
|
1082
|
+
this.reason = reason;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
var NoWalletAvailable = class extends Error {
|
|
1086
|
+
wallets;
|
|
1087
|
+
constructor(wallets) {
|
|
1088
|
+
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
1089
|
+
this.wallets = wallets;
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// ../core/lnurl.ts
|
|
1094
|
+
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1095
|
+
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1096
|
+
var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
1097
|
+
var MIN_PACE_SECS = 1;
|
|
1098
|
+
var MAX_PACE_SECS = 3600;
|
|
1099
|
+
var MIN_PER_SECOND = 0.01;
|
|
1100
|
+
var MAX_PER_SECOND = 100;
|
|
1101
|
+
async function resolve(send, addresses, amountMsat) {
|
|
1102
|
+
const served = await firstThatServes(
|
|
1103
|
+
addresses,
|
|
1104
|
+
(address, deadline) => resolveAddress(send, address, amountMsat, deadline)
|
|
1105
|
+
);
|
|
1106
|
+
return served.won;
|
|
1107
|
+
}
|
|
1108
|
+
async function firstThatServes(addresses, attempt) {
|
|
1109
|
+
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1110
|
+
const refusals3 = [];
|
|
1111
|
+
for (const address of addresses) {
|
|
1112
|
+
if (deadline.aborted) {
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
try {
|
|
1116
|
+
return { won: await attempt(address, deadline), refusals: refusals3 };
|
|
1117
|
+
} catch (refusal) {
|
|
1118
|
+
if (!(refusal instanceof WalletRefused)) {
|
|
1119
|
+
throw refusal;
|
|
1120
|
+
}
|
|
1121
|
+
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1122
|
+
refusals3.push({ address, reason: refusal.reason });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
throw new NoWalletAvailable(refusals3);
|
|
677
1126
|
}
|
|
678
|
-
function
|
|
679
|
-
|
|
1127
|
+
async function probe(send, address, amountMsat, deadline) {
|
|
1128
|
+
const [user, domain] = splitAddress(address);
|
|
1129
|
+
if (cannotReleaseAPreimage(domain)) {
|
|
1130
|
+
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1131
|
+
}
|
|
1132
|
+
const pay = await answered(
|
|
1133
|
+
send,
|
|
1134
|
+
`https://${domain}/.well-known/lnurlp/${user}`,
|
|
1135
|
+
deadline
|
|
1136
|
+
);
|
|
1137
|
+
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1138
|
+
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1139
|
+
}
|
|
1140
|
+
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1141
|
+
throw new WalletRefused(
|
|
1142
|
+
"amount-not-accepted",
|
|
1143
|
+
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return { address, pay };
|
|
680
1147
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1148
|
+
async function resolveAddress(send, address, amountMsat, deadline) {
|
|
1149
|
+
const { pay } = await probe(send, address, amountMsat, deadline);
|
|
1150
|
+
const invoice = await answered(
|
|
1151
|
+
send,
|
|
1152
|
+
withAmount(pay.callback, amountMsat),
|
|
1153
|
+
deadline
|
|
1154
|
+
);
|
|
1155
|
+
if (!invoice.pr) {
|
|
1156
|
+
throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1157
|
+
}
|
|
1158
|
+
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1159
|
+
throw new WalletRefused(
|
|
1160
|
+
"cannot-prove-delivery",
|
|
1161
|
+
`${address} publishes no usable LUD-21 verify URL`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
const issued = decodeIssued(address, invoice.pr);
|
|
1165
|
+
if (issued.amountMsat !== amountMsat) {
|
|
1166
|
+
throw new WalletRefused(
|
|
1167
|
+
"invoice-refused",
|
|
1168
|
+
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1172
|
+
throw new WalletRefused(
|
|
1173
|
+
"invoice-refused",
|
|
1174
|
+
`${address} does not bind its invoice to the metadata it serves`
|
|
1175
|
+
);
|
|
691
1176
|
}
|
|
692
|
-
const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
|
|
693
|
-
wrapped.set(PKCS8_HEADER);
|
|
694
|
-
wrapped.set(seed, PKCS8_HEADER.length);
|
|
695
|
-
const key = await crypto.subtle.importKey("pkcs8", asBuffer2(wrapped), ALGORITHM, true, ["sign"]);
|
|
696
|
-
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
697
1177
|
return {
|
|
698
|
-
|
|
699
|
-
|
|
1178
|
+
address,
|
|
1179
|
+
bolt11: invoice.pr,
|
|
1180
|
+
verifyUrl: invoice.verify,
|
|
1181
|
+
paymentHash: issued.paymentHash,
|
|
1182
|
+
expiresAt: issued.expiresAt
|
|
700
1183
|
};
|
|
701
1184
|
}
|
|
702
|
-
async function
|
|
703
|
-
|
|
704
|
-
return
|
|
1185
|
+
async function answered(send, url, deadline) {
|
|
1186
|
+
try {
|
|
1187
|
+
return await fetchJson(send, url, deadline);
|
|
1188
|
+
} catch (cause) {
|
|
1189
|
+
throw new WalletRefused("unreachable", String(cause));
|
|
705
1190
|
}
|
|
706
|
-
|
|
707
|
-
|
|
1191
|
+
}
|
|
1192
|
+
function decodeIssued(address, bolt11) {
|
|
1193
|
+
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1194
|
+
if (paymentHash === null || expiresAt === null) {
|
|
1195
|
+
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1196
|
+
}
|
|
1197
|
+
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1198
|
+
}
|
|
1199
|
+
async function checkSettled(send, verifyUrl, paymentHash) {
|
|
1200
|
+
const answer = await answeredJson(send, verifyUrl);
|
|
1201
|
+
const asked = {
|
|
1202
|
+
pace: paceAskedFor(answer.headers),
|
|
1203
|
+
ceiling: ceilingAskedFor(answer.headers)
|
|
1204
|
+
};
|
|
1205
|
+
if (!answer.said.settled || !answer.said.preimage) {
|
|
1206
|
+
return { preimage: null, ...asked };
|
|
1207
|
+
}
|
|
1208
|
+
if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
|
|
1209
|
+
throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
|
|
1210
|
+
}
|
|
1211
|
+
return { preimage: answer.said.preimage, ...asked };
|
|
1212
|
+
}
|
|
1213
|
+
function paceAskedFor(headers) {
|
|
1214
|
+
const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
|
|
1215
|
+
if (!asked) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
|
|
1219
|
+
}
|
|
1220
|
+
function ceilingAskedFor(headers) {
|
|
1221
|
+
const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
|
|
1222
|
+
if (!asked) {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
const perWindow = Number(asked[1]);
|
|
1226
|
+
const window = asked[2] === void 0 ? 1 : Number(asked[2]);
|
|
1227
|
+
if (perWindow === 0 || window === 0) {
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
|
|
1231
|
+
}
|
|
1232
|
+
function cannotReleaseAPreimage(host) {
|
|
1233
|
+
const lowered = host.toLowerCase();
|
|
1234
|
+
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1235
|
+
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
function withAmount(callback, amountMsat) {
|
|
1239
|
+
const separator = callback.includes("?") ? "&" : "?";
|
|
1240
|
+
return `${callback}${separator}amount=${amountMsat}`;
|
|
1241
|
+
}
|
|
1242
|
+
function splitAddress(address) {
|
|
1243
|
+
const [user, domain, ...rest] = address.split("@");
|
|
1244
|
+
if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
|
|
1245
|
+
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
708
1246
|
}
|
|
1247
|
+
return [user, domain];
|
|
1248
|
+
}
|
|
1249
|
+
function isNothingButAHost(domain) {
|
|
709
1250
|
try {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
713
|
-
ALGORITHM,
|
|
714
|
-
false,
|
|
715
|
-
["verify"]
|
|
716
|
-
);
|
|
717
|
-
return await crypto.subtle.verify(
|
|
718
|
-
ALGORITHM,
|
|
719
|
-
key,
|
|
720
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
721
|
-
asBuffer2(payload)
|
|
722
|
-
);
|
|
1251
|
+
const parsed = new URL(`https://${domain}/`);
|
|
1252
|
+
return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
|
|
723
1253
|
} catch {
|
|
724
1254
|
return false;
|
|
725
1255
|
}
|
|
726
1256
|
}
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1257
|
+
async function fetchJson(send, url, deadline) {
|
|
1258
|
+
return (await answeredJson(send, url, deadline)).said;
|
|
1259
|
+
}
|
|
1260
|
+
async function answeredJson(send, url, deadline) {
|
|
1261
|
+
const answer = await ask(send, url, { headers: { accept: "application/json" }, deadline });
|
|
1262
|
+
if (!answer.ok) {
|
|
1263
|
+
throw new Error(`${url} answered ${answer.status}`);
|
|
733
1264
|
}
|
|
734
|
-
|
|
1265
|
+
if (answer.truncated) {
|
|
1266
|
+
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
1267
|
+
}
|
|
1268
|
+
return { said: JSON.parse(answer.body), headers: answer.headers };
|
|
735
1269
|
}
|
|
736
|
-
|
|
737
|
-
|
|
1270
|
+
|
|
1271
|
+
// ../core/pinned.ts
|
|
1272
|
+
var HTTPS_PORT = 443;
|
|
1273
|
+
var CARRIES_NO_BODY = [204, 205, 304];
|
|
1274
|
+
var pinnedToTheAddressWeVerified = async (url, sent, signal, at) => {
|
|
1275
|
+
const asked = new URL(url);
|
|
1276
|
+
const first = at[0];
|
|
1277
|
+
if (first === void 0) {
|
|
1278
|
+
throw new Error(`${url} resolved to nothing worth connecting to`);
|
|
1279
|
+
}
|
|
1280
|
+
const { request } = await import("https");
|
|
1281
|
+
const { Readable } = await import("stream");
|
|
1282
|
+
return new Promise((settle, fail) => {
|
|
1283
|
+
if (signal.aborted) {
|
|
1284
|
+
fail(signal.reason);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const call = request(reaching(asked, sent, at, first), (answer) => {
|
|
1288
|
+
const status = answer.statusCode ?? 502;
|
|
1289
|
+
settle(
|
|
1290
|
+
new Response(
|
|
1291
|
+
CARRIES_NO_BODY.includes(status) ? null : Readable.toWeb(answer),
|
|
1292
|
+
{ status, headers: headersOf(answer.headers) }
|
|
1293
|
+
)
|
|
1294
|
+
);
|
|
1295
|
+
});
|
|
1296
|
+
const give = () => call.destroy(new Error(`${url} took longer than allowed`));
|
|
1297
|
+
signal.addEventListener("abort", give, { once: true });
|
|
1298
|
+
call.on("close", () => signal.removeEventListener("abort", give));
|
|
1299
|
+
call.on("error", fail);
|
|
1300
|
+
if (sent.body !== void 0) {
|
|
1301
|
+
call.write(sent.body);
|
|
1302
|
+
}
|
|
1303
|
+
call.end();
|
|
1304
|
+
});
|
|
1305
|
+
};
|
|
1306
|
+
function reaching(asked, sent, at, first) {
|
|
1307
|
+
return {
|
|
1308
|
+
host: asked.hostname,
|
|
1309
|
+
servername: asked.hostname,
|
|
1310
|
+
port: asked.port === "" ? HTTPS_PORT : Number(asked.port),
|
|
1311
|
+
path: `${asked.pathname}${asked.search}`,
|
|
1312
|
+
method: sent.method ?? "GET",
|
|
1313
|
+
headers: { ...sent.headers, host: asked.host },
|
|
1314
|
+
agent: false,
|
|
1315
|
+
lookup: (_name, options, done) => {
|
|
1316
|
+
if (options.all === true) {
|
|
1317
|
+
done(
|
|
1318
|
+
null,
|
|
1319
|
+
at.map((one) => ({ ...one }))
|
|
1320
|
+
);
|
|
1321
|
+
} else {
|
|
1322
|
+
done(null, first.address, first.family);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function headersOf(raw) {
|
|
1328
|
+
const headers = new Headers();
|
|
1329
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
1330
|
+
if (Array.isArray(value)) {
|
|
1331
|
+
for (const one of value) {
|
|
1332
|
+
headers.append(name, one);
|
|
1333
|
+
}
|
|
1334
|
+
} else if (value !== void 0) {
|
|
1335
|
+
headers.set(name, value);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return headers;
|
|
738
1339
|
}
|
|
739
1340
|
|
|
740
1341
|
// src/wire.ts
|
|
@@ -747,19 +1348,19 @@ var REASONS = /* @__PURE__ */ new Set([
|
|
|
747
1348
|
"cannot-prove-delivery",
|
|
748
1349
|
"invoice-refused"
|
|
749
1350
|
]);
|
|
750
|
-
function createRequestBody(
|
|
1351
|
+
function createRequestBody(priced2, webhookUrl, trigger, replay) {
|
|
751
1352
|
return JSON.stringify({
|
|
752
|
-
ln_addresses:
|
|
753
|
-
incoming_amount: toAmount(
|
|
754
|
-
webhook:
|
|
1353
|
+
ln_addresses: priced2.paidTo,
|
|
1354
|
+
incoming_amount: toAmount(priced2.amountMsat),
|
|
1355
|
+
webhook: webhookUrl ? { url: webhookUrl } : void 0,
|
|
755
1356
|
trigger: trigger ?? void 0,
|
|
756
1357
|
replay
|
|
757
1358
|
});
|
|
758
1359
|
}
|
|
759
|
-
function quoteRequestBody(
|
|
1360
|
+
function quoteRequestBody(priced2) {
|
|
760
1361
|
return JSON.stringify({
|
|
761
|
-
ln_addresses:
|
|
762
|
-
amount: toAmount(
|
|
1362
|
+
ln_addresses: priced2.paidTo,
|
|
1363
|
+
amount: toAmount(priced2.amountMsat)
|
|
763
1364
|
});
|
|
764
1365
|
}
|
|
765
1366
|
function quoteFromWire(body) {
|
|
@@ -767,12 +1368,12 @@ function quoteFromWire(body) {
|
|
|
767
1368
|
if (wire === null) {
|
|
768
1369
|
return null;
|
|
769
1370
|
}
|
|
770
|
-
const lnAddress =
|
|
1371
|
+
const lnAddress = text2(wire["ln_address"]);
|
|
771
1372
|
const amountMsat = msatFrom(wire["amount"], 1);
|
|
772
1373
|
const feeMsat = msatFrom(wire["fee"], 0);
|
|
773
1374
|
const minMsat = msatFrom(wire["min_amount"], 0);
|
|
774
1375
|
const maxMsat = msatFrom(wire["max_amount"], 0);
|
|
775
|
-
const metadata =
|
|
1376
|
+
const metadata = text2(wire["metadata"]);
|
|
776
1377
|
const refusals3 = refusalsFrom(wire["refusals"]);
|
|
777
1378
|
if (lnAddress === null || amountMsat === null || feeMsat === null) {
|
|
778
1379
|
return null;
|
|
@@ -787,57 +1388,66 @@ function paymentFromWire(body) {
|
|
|
787
1388
|
if (wire === null) {
|
|
788
1389
|
return null;
|
|
789
1390
|
}
|
|
790
|
-
const id =
|
|
791
|
-
const
|
|
792
|
-
const
|
|
793
|
-
const paymentHash = text(wire["payment_hash"]);
|
|
794
|
-
const verifyUrl = text(wire["verify_url"]);
|
|
795
|
-
const amountMsat = msatFrom(wire["incoming_amount"], 1);
|
|
1391
|
+
const id = text2(wire["id"]);
|
|
1392
|
+
const paymentHash = text2(wire["payment_hash"]);
|
|
1393
|
+
const verifyUrl = text2(wire["verify_url"]);
|
|
796
1394
|
const expiresAt = secondsFrom(wire["expires_at"]);
|
|
797
1395
|
const createdAt = secondsFrom(wire["created_at"]);
|
|
798
1396
|
const status = wire["status"];
|
|
799
1397
|
const preimage = wire["preimage"] ?? null;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
if (paymentHash === null || verifyUrl === null) {
|
|
1398
|
+
const sealed = wire["sealed"] ?? null;
|
|
1399
|
+
if (id === null || paymentHash === null || verifyUrl === null) {
|
|
804
1400
|
return null;
|
|
805
1401
|
}
|
|
806
|
-
if (
|
|
1402
|
+
if (expiresAt === null || createdAt === null || !isStatus(status)) {
|
|
807
1403
|
return null;
|
|
808
1404
|
}
|
|
809
|
-
if (
|
|
1405
|
+
if (preimage !== null && typeof preimage !== "string") {
|
|
810
1406
|
return null;
|
|
811
1407
|
}
|
|
812
|
-
if (
|
|
1408
|
+
if (sealed !== null && typeof sealed !== "string") {
|
|
813
1409
|
return null;
|
|
814
1410
|
}
|
|
815
|
-
|
|
1411
|
+
const reported = {
|
|
816
1412
|
id,
|
|
817
|
-
lnAddress,
|
|
818
|
-
amountMsat,
|
|
819
1413
|
status,
|
|
820
1414
|
paymentHash,
|
|
821
|
-
|
|
1415
|
+
verifyUrl,
|
|
822
1416
|
preimage,
|
|
823
1417
|
expiresAt,
|
|
824
1418
|
createdAt,
|
|
825
|
-
|
|
1419
|
+
sealed
|
|
826
1420
|
};
|
|
1421
|
+
const lnAddress = text2(wire["ln_address"]);
|
|
1422
|
+
const amountMsat = wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1);
|
|
1423
|
+
const bolt11 = text2(wire["bolt11"]);
|
|
1424
|
+
if (lnAddress !== null && amountMsat !== null && bolt11 !== null) {
|
|
1425
|
+
return { ...reported, kind: "minted", lnAddress, amountMsat, bolt11 };
|
|
1426
|
+
}
|
|
1427
|
+
if (lnAddress === null && amountMsat === null && bolt11 === null) {
|
|
1428
|
+
return { ...reported, kind: "watched", lnAddress: null, amountMsat: null, bolt11: null };
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
function mintedFromWire(body) {
|
|
1433
|
+
const payment = paymentFromWire(body);
|
|
1434
|
+
return payment !== null && payment.kind === "minted" ? payment : null;
|
|
827
1435
|
}
|
|
828
|
-
function watchRequestBody(
|
|
829
|
-
const expiresAt = new Date(
|
|
1436
|
+
function watchRequestBody(handover, trigger) {
|
|
1437
|
+
const expiresAt = new Date(handover.expiresAt * 1e3);
|
|
830
1438
|
if (Number.isNaN(expiresAt.getTime())) {
|
|
831
|
-
throw new TypeError(
|
|
1439
|
+
throw new TypeError(
|
|
1440
|
+
`expiresAt must be a usable unix time in seconds, got ${handover.expiresAt}`
|
|
1441
|
+
);
|
|
832
1442
|
}
|
|
833
1443
|
return JSON.stringify({
|
|
834
|
-
payment_hash:
|
|
835
|
-
verify_url:
|
|
1444
|
+
payment_hash: handover.paymentHash,
|
|
1445
|
+
verify_url: handover.verifyUrl,
|
|
836
1446
|
expires_at: expiresAt.toISOString(),
|
|
837
1447
|
trigger: trigger ?? void 0,
|
|
838
|
-
replay:
|
|
839
|
-
sealed:
|
|
840
|
-
webhook:
|
|
1448
|
+
replay: handover.replay,
|
|
1449
|
+
sealed: handover.sealed,
|
|
1450
|
+
webhook: handover.webhookUrl ? { url: handover.webhookUrl } : void 0
|
|
841
1451
|
});
|
|
842
1452
|
}
|
|
843
1453
|
function settlementFromWire(body) {
|
|
@@ -845,8 +1455,8 @@ function settlementFromWire(body) {
|
|
|
845
1455
|
if (wire === null) {
|
|
846
1456
|
return null;
|
|
847
1457
|
}
|
|
848
|
-
const id =
|
|
849
|
-
const paymentHash =
|
|
1458
|
+
const id = text2(wire["id"]);
|
|
1459
|
+
const paymentHash = text2(wire["payment_hash"]);
|
|
850
1460
|
const settledAt = secondsFrom(wire["settled_at"]);
|
|
851
1461
|
const status = wire["status"];
|
|
852
1462
|
const preimage = wire["preimage"] ?? null;
|
|
@@ -858,66 +1468,20 @@ function settlementFromWire(body) {
|
|
|
858
1468
|
}
|
|
859
1469
|
return { id, status, paymentHash, preimage, settledAt };
|
|
860
1470
|
}
|
|
861
|
-
function triggerEventFromWire(body) {
|
|
862
|
-
const wire = asObject(body);
|
|
863
|
-
if (wire === null) {
|
|
864
|
-
return null;
|
|
865
|
-
}
|
|
866
|
-
const id = text(wire["id"]);
|
|
867
|
-
const paymentHash = text(wire["payment_hash"]);
|
|
868
|
-
const verifyUrl = text(wire["verify_url"]);
|
|
869
|
-
const expiresAt = secondsFrom(wire["expires_at"]);
|
|
870
|
-
const createdAt = secondsFrom(wire["created_at"]);
|
|
871
|
-
const status = wire["status"];
|
|
872
|
-
const preimage = wire["preimage"] ?? null;
|
|
873
|
-
const sealed = wire["sealed"] ?? null;
|
|
874
|
-
if (id === null || paymentHash === null || verifyUrl === null) {
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
if (expiresAt === null || createdAt === null || !isStatus(status)) {
|
|
878
|
-
return null;
|
|
879
|
-
}
|
|
880
|
-
if (preimage !== null && typeof preimage !== "string") {
|
|
881
|
-
return null;
|
|
882
|
-
}
|
|
883
|
-
if (sealed !== null && typeof sealed !== "string") {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
return {
|
|
887
|
-
id,
|
|
888
|
-
kind: kindOf(wire),
|
|
889
|
-
paymentHash,
|
|
890
|
-
verifyUrl,
|
|
891
|
-
status,
|
|
892
|
-
preimage,
|
|
893
|
-
expiresAt,
|
|
894
|
-
createdAt,
|
|
895
|
-
sealed,
|
|
896
|
-
lnAddress: text(wire["ln_address"]),
|
|
897
|
-
amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
1471
|
function socketTicketFromWire(body) {
|
|
901
1472
|
const wire = asObject(body);
|
|
902
1473
|
if (wire === null) {
|
|
903
1474
|
return null;
|
|
904
1475
|
}
|
|
905
|
-
const ticket =
|
|
1476
|
+
const ticket = text2(wire["ticket"]);
|
|
906
1477
|
const expiresAt = secondsFrom(wire["expires_at"]);
|
|
907
1478
|
if (ticket === null || expiresAt === null) {
|
|
908
1479
|
return null;
|
|
909
1480
|
}
|
|
910
1481
|
return { ticket, expiresAt };
|
|
911
1482
|
}
|
|
912
|
-
function
|
|
913
|
-
|
|
914
|
-
if (said === "minted" || said === "watched") {
|
|
915
|
-
return said;
|
|
916
|
-
}
|
|
917
|
-
return text(wire["ln_address"]) === null ? "watched" : "minted";
|
|
918
|
-
}
|
|
919
|
-
function toAmount(msat) {
|
|
920
|
-
return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
|
|
1483
|
+
function toAmount(msat2) {
|
|
1484
|
+
return { value: String(msat2), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
|
|
921
1485
|
}
|
|
922
1486
|
function asObject(value) {
|
|
923
1487
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -925,7 +1489,7 @@ function asObject(value) {
|
|
|
925
1489
|
}
|
|
926
1490
|
return value;
|
|
927
1491
|
}
|
|
928
|
-
function
|
|
1492
|
+
function text2(value) {
|
|
929
1493
|
return typeof value === "string" ? value : null;
|
|
930
1494
|
}
|
|
931
1495
|
function msatFrom(value, least) {
|
|
@@ -940,8 +1504,8 @@ function msatFrom(value, least) {
|
|
|
940
1504
|
if (typeof digits !== "string" || !/^[0-9]+$/.test(digits)) {
|
|
941
1505
|
return null;
|
|
942
1506
|
}
|
|
943
|
-
const
|
|
944
|
-
return Number.isSafeInteger(
|
|
1507
|
+
const msat2 = Number(digits);
|
|
1508
|
+
return Number.isSafeInteger(msat2) && msat2 >= least ? msat2 : null;
|
|
945
1509
|
}
|
|
946
1510
|
function refusalsFrom(value) {
|
|
947
1511
|
if (!Array.isArray(value)) {
|
|
@@ -950,7 +1514,7 @@ function refusalsFrom(value) {
|
|
|
950
1514
|
const refusals3 = [];
|
|
951
1515
|
for (const entry of value) {
|
|
952
1516
|
const failure = asObject(entry);
|
|
953
|
-
const address = failure === null ? null :
|
|
1517
|
+
const address = failure === null ? null : text2(failure["address"]);
|
|
954
1518
|
const reason = failure?.["reason"];
|
|
955
1519
|
if (address === null || typeof reason !== "string" || !REASONS.has(reason)) {
|
|
956
1520
|
return null;
|
|
@@ -971,118 +1535,67 @@ function isStatus(value) {
|
|
|
971
1535
|
}
|
|
972
1536
|
|
|
973
1537
|
// src/webhook.ts
|
|
974
|
-
var
|
|
975
|
-
var
|
|
1538
|
+
var SIGNATURE_HEADER2 = "x-signature";
|
|
1539
|
+
var TIMESTAMP_HEADER2 = "x-timestamp";
|
|
976
1540
|
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
977
1541
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
978
1542
|
var CHALLENGE = "webhook-challenge";
|
|
979
1543
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
980
|
-
async function
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
|
|
985
|
-
return false;
|
|
986
|
-
}
|
|
987
|
-
return await verifyHex(
|
|
988
|
-
credential.publicKey.toLowerCase(),
|
|
989
|
-
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
990
|
-
signed(timestamp, body)
|
|
991
|
-
);
|
|
1544
|
+
async function readPayment(request, credential, options = {}) {
|
|
1545
|
+
const body = await believable(request, credential, options);
|
|
1546
|
+
return body === null ? null : decoded(body, paymentFromWire);
|
|
992
1547
|
}
|
|
993
|
-
async function
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
998
|
-
try {
|
|
999
|
-
return paymentFromWire(JSON.parse(text2));
|
|
1000
|
-
} catch {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1548
|
+
async function readSettlement(request, credential, options = {}) {
|
|
1549
|
+
const body = await believable(request, credential, options);
|
|
1550
|
+
return body === null ? null : decoded(body, settlementFromWire);
|
|
1003
1551
|
}
|
|
1004
|
-
async function
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
return null;
|
|
1009
|
-
}
|
|
1010
|
-
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1552
|
+
async function answerWebhookChallenge(request, credential, options = {}) {
|
|
1553
|
+
const body = await believable(request, credential, options);
|
|
1554
|
+
const nonce = body === null ? null : nonceOf(body, CHALLENGE);
|
|
1555
|
+
return nonce === null ? null : answered2(nonce);
|
|
1011
1556
|
}
|
|
1012
|
-
async function
|
|
1013
|
-
if (
|
|
1014
|
-
return null;
|
|
1015
|
-
}
|
|
1016
|
-
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1017
|
-
try {
|
|
1018
|
-
return triggerEventFromWire(JSON.parse(text2));
|
|
1019
|
-
} catch {
|
|
1557
|
+
async function answerVerifyChallenge(request) {
|
|
1558
|
+
if (request.method !== "POST") {
|
|
1020
1559
|
return null;
|
|
1021
1560
|
}
|
|
1561
|
+
const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
|
|
1562
|
+
return nonce === null ? null : answered2(nonce);
|
|
1022
1563
|
}
|
|
1023
|
-
async function
|
|
1024
|
-
const signature = request.headers.get(
|
|
1025
|
-
const timestamp = request.headers.get(
|
|
1564
|
+
async function believable(request, credential, options) {
|
|
1565
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1566
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1026
1567
|
if (signature === null || timestamp === null) {
|
|
1027
1568
|
return null;
|
|
1028
1569
|
}
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
async function parseSettlement(body, signature, credential, timestamp, options = {}) {
|
|
1032
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
|
|
1033
|
-
return null;
|
|
1034
|
-
}
|
|
1035
|
-
try {
|
|
1036
|
-
return settlementFromWire(JSON.parse(textOf(body)));
|
|
1037
|
-
} catch {
|
|
1038
|
-
return null;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
async function parseSettlementRequest(request, credential, options = {}) {
|
|
1042
|
-
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1043
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1044
|
-
if (signature === null || timestamp === null) {
|
|
1570
|
+
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) {
|
|
1045
1571
|
return null;
|
|
1046
1572
|
}
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
function isProvablySettled(settled) {
|
|
1050
|
-
if (settled.status !== "paid" || settled.preimage === null) {
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
return preimageMatchesHash(settled.preimage, settled.paymentHash);
|
|
1054
|
-
}
|
|
1055
|
-
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1056
|
-
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) {
|
|
1573
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
|
|
1057
1574
|
return null;
|
|
1058
1575
|
}
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1576
|
+
const body = await request.clone().text();
|
|
1577
|
+
const signed = await verifyHex(
|
|
1578
|
+
credential.publicKey.toLowerCase(),
|
|
1579
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1580
|
+
stamped(timestamp, body)
|
|
1581
|
+
);
|
|
1582
|
+
return signed ? body : null;
|
|
1061
1583
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1584
|
+
function decoded(body, from) {
|
|
1585
|
+
try {
|
|
1586
|
+
return from(JSON.parse(body));
|
|
1587
|
+
} catch {
|
|
1066
1588
|
return null;
|
|
1067
1589
|
}
|
|
1068
|
-
const answer = await answerWebhookChallenge(
|
|
1069
|
-
await request.clone().text(),
|
|
1070
|
-
signature,
|
|
1071
|
-
credential,
|
|
1072
|
-
timestamp,
|
|
1073
|
-
options
|
|
1074
|
-
);
|
|
1075
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1076
|
-
}
|
|
1077
|
-
function textOf(body) {
|
|
1078
|
-
return typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1079
1590
|
}
|
|
1080
|
-
function
|
|
1081
|
-
return
|
|
1591
|
+
function answered2(nonce) {
|
|
1592
|
+
return new Response(JSON.stringify({ nonce }), {
|
|
1593
|
+
headers: { "content-type": "application/json" }
|
|
1594
|
+
});
|
|
1082
1595
|
}
|
|
1083
|
-
function nonceOf(
|
|
1596
|
+
function nonceOf(text3, type) {
|
|
1084
1597
|
try {
|
|
1085
|
-
const said = JSON.parse(
|
|
1598
|
+
const said = JSON.parse(text3);
|
|
1086
1599
|
if (said["type"] !== type || typeof said["nonce"] !== "string") {
|
|
1087
1600
|
return null;
|
|
1088
1601
|
}
|
|
@@ -1091,17 +1604,6 @@ function nonceOf(text2, type) {
|
|
|
1091
1604
|
return null;
|
|
1092
1605
|
}
|
|
1093
1606
|
}
|
|
1094
|
-
function answerVerifyChallenge(body) {
|
|
1095
|
-
const nonce = nonceOf(body, VERIFY_CHALLENGE);
|
|
1096
|
-
return nonce === null ? null : JSON.stringify({ nonce });
|
|
1097
|
-
}
|
|
1098
|
-
async function answerVerifyChallengeRequest(request) {
|
|
1099
|
-
if (request.method !== "POST") {
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
const answer = answerVerifyChallenge(await request.clone().text());
|
|
1103
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1104
|
-
}
|
|
1105
1607
|
function recent(timestamp, toleranceSecs) {
|
|
1106
1608
|
const sent = Number(timestamp);
|
|
1107
1609
|
if (!Number.isFinite(sent)) {
|
|
@@ -1109,14 +1611,8 @@ function recent(timestamp, toleranceSecs) {
|
|
|
1109
1611
|
}
|
|
1110
1612
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1111
1613
|
}
|
|
1112
|
-
function
|
|
1113
|
-
|
|
1114
|
-
const stamp = encoder.encode(`${timestamp}.`);
|
|
1115
|
-
const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
|
|
1116
|
-
const payload = new Uint8Array(stamp.length + rest.length);
|
|
1117
|
-
payload.set(stamp);
|
|
1118
|
-
payload.set(rest, stamp.length);
|
|
1119
|
-
return payload;
|
|
1614
|
+
function stamped(timestamp, body) {
|
|
1615
|
+
return new TextEncoder().encode(`${timestamp}.${body}`);
|
|
1120
1616
|
}
|
|
1121
1617
|
|
|
1122
1618
|
// src/bank.ts
|
|
@@ -1125,10 +1621,10 @@ var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
|
1125
1621
|
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1126
1622
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1127
1623
|
var FORBIDDEN_IN_SPD = "*";
|
|
1128
|
-
async function bankTransfer(params) {
|
|
1624
|
+
async function bankTransfer(gateway, params) {
|
|
1129
1625
|
const currency = params.currency ?? DEFAULT_CURRENCY;
|
|
1130
1626
|
refuseUnusable(params, currency);
|
|
1131
|
-
await refuseAnOpenGateway(params);
|
|
1627
|
+
await refuseAnOpenGateway(gateway, params);
|
|
1132
1628
|
const spd = shortPaymentDescriptor(params, currency);
|
|
1133
1629
|
const subject = subjectOf(params.reference, params.amountMinor, currency);
|
|
1134
1630
|
const polling = new URL(params.verifyUrl);
|
|
@@ -1137,7 +1633,7 @@ async function bankTransfer(params) {
|
|
|
1137
1633
|
polling.searchParams.set("cc", currency);
|
|
1138
1634
|
polling.searchParams.set("sig", await hmacHex(params.secret, `verify|${subject}`));
|
|
1139
1635
|
const verifyUrl = polling.toString();
|
|
1140
|
-
const watched = await
|
|
1636
|
+
const watched = await gateway.watch({
|
|
1141
1637
|
paymentHash: hashOf(await hmacHex(params.secret, `preimage|${subject}`)),
|
|
1142
1638
|
verifyUrl,
|
|
1143
1639
|
expiresAt: params.expiresAt,
|
|
@@ -1153,7 +1649,7 @@ function bankVerifyEndpoint(config) {
|
|
|
1153
1649
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1154
1650
|
};
|
|
1155
1651
|
return async (request) => {
|
|
1156
|
-
const consented = await
|
|
1652
|
+
const consented = await answerVerifyChallenge(request);
|
|
1157
1653
|
if (consented !== null) {
|
|
1158
1654
|
return consented;
|
|
1159
1655
|
}
|
|
@@ -1214,11 +1710,11 @@ function shortPaymentDescriptor(params, currency) {
|
|
|
1214
1710
|
function major(amountMinor, currency) {
|
|
1215
1711
|
return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
|
|
1216
1712
|
}
|
|
1217
|
-
async function refuseAnOpenGateway(params) {
|
|
1713
|
+
async function refuseAnOpenGateway(gateway, params) {
|
|
1218
1714
|
if (params.allowPublicGateway === true) {
|
|
1219
1715
|
return;
|
|
1220
1716
|
}
|
|
1221
|
-
if (await
|
|
1717
|
+
if (await gateway.refusesStrangers()) {
|
|
1222
1718
|
return;
|
|
1223
1719
|
}
|
|
1224
1720
|
throw new Error(
|
|
@@ -1242,124 +1738,240 @@ function refuseUnusable(params, currency) {
|
|
|
1242
1738
|
throw new Error("a variable symbol is up to ten digits");
|
|
1243
1739
|
}
|
|
1244
1740
|
}
|
|
1245
|
-
function unixNow() {
|
|
1246
|
-
return Math.floor(Date.now() / 1e3);
|
|
1741
|
+
function unixNow() {
|
|
1742
|
+
return Math.floor(Date.now() / 1e3);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/qr.ts
|
|
1746
|
+
var import_uqr = require("uqr");
|
|
1747
|
+
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1748
|
+
function toLightningUri(destination) {
|
|
1749
|
+
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1750
|
+
return `LIGHTNING:${scanned}`;
|
|
1751
|
+
}
|
|
1752
|
+
function invoiceToSvg(destination, options) {
|
|
1753
|
+
return svgOf(toLightningUri(destination), options);
|
|
1754
|
+
}
|
|
1755
|
+
function svgOf(scanned, options) {
|
|
1756
|
+
const size = options?.size ?? 256;
|
|
1757
|
+
const color = options?.color ?? "#000";
|
|
1758
|
+
if (!SAFE_COLOR.test(color)) {
|
|
1759
|
+
throw new Error(`Invalid color: ${color}`);
|
|
1760
|
+
}
|
|
1761
|
+
return renderSvg(scanned, size, color);
|
|
1762
|
+
}
|
|
1763
|
+
function isLnAddress(destination) {
|
|
1764
|
+
const at = destination.indexOf("@");
|
|
1765
|
+
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1766
|
+
}
|
|
1767
|
+
function renderSvg(content, size, color) {
|
|
1768
|
+
const { data } = (0, import_uqr.encode)(content);
|
|
1769
|
+
const modules = data.length;
|
|
1770
|
+
const cellSize = size / modules;
|
|
1771
|
+
let path = "";
|
|
1772
|
+
for (let y = 0; y < modules; y++) {
|
|
1773
|
+
for (let x = 0; x < modules; x++) {
|
|
1774
|
+
if (data[y][x]) {
|
|
1775
|
+
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return [
|
|
1780
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1781
|
+
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1782
|
+
`<path d="${path}" fill="${color}"/>`,
|
|
1783
|
+
`</svg>`
|
|
1784
|
+
].join("");
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/relay.ts
|
|
1788
|
+
var DEFAULT_POLL_EVERY_SECS2 = 5;
|
|
1789
|
+
var WALLET = "w";
|
|
1790
|
+
function lightningVerifyEndpoint(config) {
|
|
1791
|
+
const paced = {
|
|
1792
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
|
|
1793
|
+
};
|
|
1794
|
+
return async (request) => {
|
|
1795
|
+
const consented = await answerVerifyChallenge(request);
|
|
1796
|
+
if (consented !== null) {
|
|
1797
|
+
return consented;
|
|
1798
|
+
}
|
|
1799
|
+
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
1800
|
+
if (sealed === null) {
|
|
1801
|
+
return Response.json({ settled: false }, { status: 400 });
|
|
1802
|
+
}
|
|
1803
|
+
const opened = await unseal(config.secret, sealed);
|
|
1804
|
+
if (opened === null) {
|
|
1805
|
+
return Response.json({ settled: false }, { status: 403 });
|
|
1806
|
+
}
|
|
1807
|
+
const wallet = JSON.parse(opened);
|
|
1808
|
+
const asked = await checkSettled(
|
|
1809
|
+
config.send ?? pinnedToTheAddressWeVerified,
|
|
1810
|
+
wallet.url,
|
|
1811
|
+
wallet.hash
|
|
1812
|
+
).catch(() => null);
|
|
1813
|
+
if (asked === null) {
|
|
1814
|
+
return Response.json({ settled: false }, { status: 502 });
|
|
1815
|
+
}
|
|
1816
|
+
return Response.json(
|
|
1817
|
+
{ settled: asked.preimage !== null, preimage: asked.preimage },
|
|
1818
|
+
{ headers: paced }
|
|
1819
|
+
);
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
1823
|
+
const relayed = new URL(endpoint);
|
|
1824
|
+
relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
|
|
1825
|
+
return relayed.toString();
|
|
1247
1826
|
}
|
|
1248
1827
|
|
|
1249
|
-
//
|
|
1250
|
-
var
|
|
1251
|
-
var
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1828
|
+
// src/rail.ts
|
|
1829
|
+
var BANK = "bank";
|
|
1830
|
+
var LIGHTNING = "lightning";
|
|
1831
|
+
function bankRail(gateway, config) {
|
|
1832
|
+
return async (order) => {
|
|
1833
|
+
const expiresAt = config.expiresAt(order);
|
|
1834
|
+
const transfer = await bankTransfer(gateway, {
|
|
1835
|
+
secret: config.secret,
|
|
1836
|
+
iban: config.iban,
|
|
1837
|
+
verifyUrl: config.verifyUrl,
|
|
1838
|
+
reference: order.reference,
|
|
1839
|
+
amountMinor: order.amountMinor,
|
|
1840
|
+
currency: order.currency,
|
|
1841
|
+
expiresAt,
|
|
1842
|
+
trigger: config.trigger,
|
|
1843
|
+
replay: config.replay,
|
|
1844
|
+
sealed: await config.sealed?.(order),
|
|
1845
|
+
variableSymbol: config.variableSymbol?.(order),
|
|
1846
|
+
webhookUrl: config.webhookUrl,
|
|
1847
|
+
allowPublicGateway: config.allowPublicGateway
|
|
1848
|
+
});
|
|
1849
|
+
return {
|
|
1850
|
+
id: transfer.id,
|
|
1851
|
+
rail: config.name ?? BANK,
|
|
1852
|
+
scan: transfer.spd,
|
|
1853
|
+
qr: transfer.spd,
|
|
1854
|
+
expiresAt
|
|
1855
|
+
};
|
|
1856
|
+
};
|
|
1257
1857
|
}
|
|
1258
|
-
function
|
|
1259
|
-
return
|
|
1858
|
+
function lightningRail(gateway, config) {
|
|
1859
|
+
return async (order) => {
|
|
1860
|
+
const payment = await gateway.mint(
|
|
1861
|
+
{
|
|
1862
|
+
paidTo: config.paidTo,
|
|
1863
|
+
amount: await msatForOrder(order, config.amount, config.rate),
|
|
1864
|
+
webhookUrl: config.webhookUrl
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
idempotencyKey: config.idempotencyKey?.(order),
|
|
1868
|
+
trigger: config.trigger,
|
|
1869
|
+
replay: config.replay
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
1872
|
+
return {
|
|
1873
|
+
id: payment.id,
|
|
1874
|
+
rail: config.name ?? LIGHTNING,
|
|
1875
|
+
scan: payment.bolt11,
|
|
1876
|
+
qr: toLightningUri(payment.bolt11),
|
|
1877
|
+
expiresAt: payment.expiresAt
|
|
1878
|
+
};
|
|
1879
|
+
};
|
|
1260
1880
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1881
|
+
function blindLightningRail(gateway, config) {
|
|
1882
|
+
return async (order) => {
|
|
1883
|
+
const resolved2 = await invoiceFrom(
|
|
1884
|
+
config.paidTo,
|
|
1885
|
+
await msatForOrder(order, config.amount, config.rate),
|
|
1886
|
+
config.send
|
|
1887
|
+
);
|
|
1888
|
+
const relay = config.relayThrough;
|
|
1889
|
+
const watched = await gateway.watch({
|
|
1890
|
+
paymentHash: resolved2.paymentHash,
|
|
1891
|
+
verifyUrl: relay ? await relayedVerifyUrl(
|
|
1892
|
+
relay.endpoint,
|
|
1893
|
+
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
1894
|
+
relay.secret
|
|
1895
|
+
) : resolved2.verifyUrl,
|
|
1896
|
+
expiresAt: resolved2.expiresAt,
|
|
1897
|
+
trigger: config.trigger,
|
|
1898
|
+
replay: config.replay,
|
|
1899
|
+
sealed: await config.sealed?.(order),
|
|
1900
|
+
webhookUrl: config.webhookUrl
|
|
1901
|
+
});
|
|
1902
|
+
return {
|
|
1903
|
+
id: watched.id,
|
|
1904
|
+
rail: config.name ?? LIGHTNING,
|
|
1905
|
+
scan: resolved2.bolt11,
|
|
1906
|
+
qr: toLightningUri(resolved2.bolt11),
|
|
1907
|
+
expiresAt: resolved2.expiresAt
|
|
1908
|
+
};
|
|
1267
1909
|
};
|
|
1268
1910
|
}
|
|
1269
|
-
function
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1911
|
+
async function invoiceFrom(paidTo, amount, send = pinnedToTheAddressWeVerified) {
|
|
1912
|
+
const addresses = typeof paidTo === "string" ? [paidTo] : paidTo;
|
|
1913
|
+
try {
|
|
1914
|
+
return await resolve(send, addresses, await amountNow(amount));
|
|
1915
|
+
} catch (refused) {
|
|
1916
|
+
if (refused instanceof NoWalletAvailable) {
|
|
1917
|
+
throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
|
|
1918
|
+
}
|
|
1919
|
+
throw refused;
|
|
1920
|
+
}
|
|
1273
1921
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
code;
|
|
1278
|
-
paymentId;
|
|
1279
|
-
constructor(code, paymentId) {
|
|
1280
|
-
super(`gateway verification failed: ${code}`);
|
|
1281
|
-
this.name = "GatewayCheatError";
|
|
1282
|
-
this.code = code;
|
|
1283
|
-
this.paymentId = paymentId;
|
|
1922
|
+
async function msatForOrder(order, amount, rate) {
|
|
1923
|
+
if (amount !== void 0) {
|
|
1924
|
+
return await amountNow(amount(order));
|
|
1284
1925
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
constructor(
|
|
1289
|
-
|
|
1290
|
-
this.name = "WrapRefusedError";
|
|
1291
|
-
this.code = code;
|
|
1926
|
+
return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
|
|
1927
|
+
}
|
|
1928
|
+
var Rails = class {
|
|
1929
|
+
constructor(gateway) {
|
|
1930
|
+
this.gateway = gateway;
|
|
1292
1931
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
constructor(lnAddress, paymentId, cause) {
|
|
1298
|
-
super(`could not reach ${lnAddress} to verify the invoice: ${String(cause)}`);
|
|
1299
|
-
this.name = "UnverifiedRecipientError";
|
|
1300
|
-
this.lnAddress = lnAddress;
|
|
1301
|
-
this.paymentId = paymentId;
|
|
1932
|
+
gateway;
|
|
1933
|
+
/** Lightning, with the gateway minting against a priority list of addresses */
|
|
1934
|
+
lightning(config) {
|
|
1935
|
+
return lightningRail(this.gateway, config);
|
|
1302
1936
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
constructor(problem) {
|
|
1310
|
-
const title = problem.title ?? "Request failed";
|
|
1311
|
-
super(problem.detail ? `${title}: ${problem.detail}` : title);
|
|
1312
|
-
this.name = "ProblemError";
|
|
1313
|
-
this.type = problem.type ?? "about:blank";
|
|
1314
|
-
this.title = title;
|
|
1315
|
-
this.status = problem.status ?? 0;
|
|
1316
|
-
this.detail = problem.detail ?? null;
|
|
1937
|
+
/**
|
|
1938
|
+
* Lightning, with the invoice resolved here so the gateway is told neither the
|
|
1939
|
+
* address nor the amount
|
|
1940
|
+
*/
|
|
1941
|
+
blindLightning(config) {
|
|
1942
|
+
return blindLightningRail(this.gateway, config);
|
|
1317
1943
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
1323
|
-
var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
1324
|
-
var IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge:idempotency-key-reused";
|
|
1325
|
-
var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
|
|
1326
|
-
var IdempotencyConflictError = class extends ProblemError {
|
|
1327
|
-
conflict;
|
|
1328
|
-
constructor(problem, conflict) {
|
|
1329
|
-
super(problem);
|
|
1330
|
-
this.name = "IdempotencyConflictError";
|
|
1331
|
-
this.conflict = conflict;
|
|
1944
|
+
/** A bank transfer, proved the way a Lightning payment is */
|
|
1945
|
+
bank(config) {
|
|
1946
|
+
return bankRail(this.gateway, config);
|
|
1332
1947
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
this.
|
|
1339
|
-
this.wallets = wallets;
|
|
1948
|
+
/**
|
|
1949
|
+
* One bank transfer without building a rail first, for a shop that asks for
|
|
1950
|
+
* them one at a time rather than beside another payment method
|
|
1951
|
+
*/
|
|
1952
|
+
transfer(params) {
|
|
1953
|
+
return bankTransfer(this.gateway, params);
|
|
1340
1954
|
}
|
|
1341
1955
|
};
|
|
1342
1956
|
|
|
1343
1957
|
// src/verify.ts
|
|
1344
|
-
var
|
|
1958
|
+
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1345
1959
|
var DEFAULT_WRAP_PROPORTION = 0.01;
|
|
1346
1960
|
var DEFAULT_WRAP_BASE_MSAT = 1e3;
|
|
1347
|
-
async function proveOrigin(payment,
|
|
1961
|
+
async function proveOrigin(payment, asked) {
|
|
1348
1962
|
const cheat = (code) => new GatewayCheatError(code, payment.id);
|
|
1349
|
-
const listed =
|
|
1350
|
-
(address) => equalIgnoringCase(address, payment.lnAddress)
|
|
1351
|
-
);
|
|
1963
|
+
const listed = asked.paidTo.find((address) => equalIgnoringCase(address, payment.lnAddress));
|
|
1352
1964
|
if (listed === void 0) {
|
|
1353
1965
|
throw cheat("address_not_requested");
|
|
1354
1966
|
}
|
|
1355
|
-
if (payment.amountMsat !==
|
|
1967
|
+
if (payment.amountMsat !== asked.amountMsat) {
|
|
1356
1968
|
throw cheat("amount_mismatch");
|
|
1357
1969
|
}
|
|
1358
1970
|
const invoice = decodeInvoice(payment.bolt11);
|
|
1359
1971
|
if (invoice.paymentHash === null || !equalIgnoringCase(invoice.paymentHash, payment.paymentHash)) {
|
|
1360
1972
|
throw cheat("hash_mismatch");
|
|
1361
1973
|
}
|
|
1362
|
-
if (invoice.amountMsat !==
|
|
1974
|
+
if (invoice.amountMsat !== asked.amountMsat) {
|
|
1363
1975
|
throw cheat("amount_mismatch");
|
|
1364
1976
|
}
|
|
1365
1977
|
const payRequest = await payRequestFor(listed, payment);
|
|
@@ -1377,8 +1989,8 @@ async function proveOrigin(payment, request) {
|
|
|
1377
1989
|
throw cheat("invoice_not_issued");
|
|
1378
1990
|
}
|
|
1379
1991
|
}
|
|
1380
|
-
async function proveSettlement(payment,
|
|
1381
|
-
await proveOrigin(payment,
|
|
1992
|
+
async function proveSettlement(payment, asked) {
|
|
1993
|
+
await proveOrigin(payment, asked);
|
|
1382
1994
|
const verified = await reachable(payment.verifyUrl, payment);
|
|
1383
1995
|
if (verified.settled !== true || typeof verified.preimage !== "string") {
|
|
1384
1996
|
return null;
|
|
@@ -1388,46 +2000,48 @@ async function proveSettlement(payment, request) {
|
|
|
1388
2000
|
}
|
|
1389
2001
|
return verified.preimage;
|
|
1390
2002
|
}
|
|
1391
|
-
function
|
|
1392
|
-
if (
|
|
2003
|
+
function carriesProof(report) {
|
|
2004
|
+
if (report.status !== "paid" || report.preimage === null) {
|
|
1393
2005
|
return false;
|
|
1394
2006
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
2007
|
+
if (report.bolt11 !== void 0 && report.bolt11 !== null) {
|
|
2008
|
+
const invoiceHash = decodeInvoice(report.bolt11).paymentHash;
|
|
2009
|
+
if (invoiceHash === null || !equalIgnoringCase(invoiceHash, report.paymentHash)) {
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
1398
2012
|
}
|
|
1399
|
-
return preimageMatchesHash(
|
|
2013
|
+
return preimageMatchesHash(report.preimage, report.paymentHash);
|
|
1400
2014
|
}
|
|
1401
2015
|
function wrapFeeCeiling(amountMsat, allowance) {
|
|
1402
2016
|
const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
|
|
1403
|
-
return Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional));
|
|
2017
|
+
return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
|
|
1404
2018
|
}
|
|
1405
2019
|
function proveWrapped(wrapped, recipient, allowance) {
|
|
1406
2020
|
const offered = decodeInvoice(wrapped);
|
|
1407
2021
|
const owed = decodeInvoice(recipient);
|
|
1408
|
-
const
|
|
2022
|
+
const refuse2 = (code, detail) => {
|
|
1409
2023
|
throw new WrapRefusedError(code, detail);
|
|
1410
2024
|
};
|
|
1411
2025
|
if (offered.paymentHash === null || offered.amountMsat === null || offered.expiresAt === null) {
|
|
1412
|
-
|
|
2026
|
+
refuse2("undecodable", "the wrapped invoice carries no hash, amount or expiry");
|
|
1413
2027
|
}
|
|
1414
2028
|
if (owed.paymentHash === null || owed.amountMsat === null || owed.expiresAt === null) {
|
|
1415
|
-
|
|
2029
|
+
refuse2("undecodable", "the recipient's invoice carries no hash, amount or expiry");
|
|
1416
2030
|
}
|
|
1417
2031
|
if (offered.paymentHash?.toLowerCase() !== owed.paymentHash?.toLowerCase()) {
|
|
1418
|
-
|
|
2032
|
+
refuse2("hash_mismatch", "settling the wrap would not settle the recipient");
|
|
1419
2033
|
}
|
|
1420
2034
|
const asked = offered.amountMsat ?? 0;
|
|
1421
2035
|
const due = owed.amountMsat ?? 0;
|
|
1422
2036
|
if (asked < due) {
|
|
1423
|
-
|
|
2037
|
+
refuse2("amount_below_recipient", `${asked} msat cannot forward ${due} msat`);
|
|
1424
2038
|
}
|
|
1425
2039
|
const ceiling = wrapFeeCeiling(due, allowance);
|
|
1426
2040
|
if (asked - due > ceiling) {
|
|
1427
|
-
|
|
2041
|
+
refuse2("fee_above_allowance", `${asked - due} msat over ${due}, and ${ceiling} was allowed`);
|
|
1428
2042
|
}
|
|
1429
2043
|
if ((owed.expiresAt ?? 0) <= (offered.expiresAt ?? 0)) {
|
|
1430
|
-
|
|
2044
|
+
refuse2("recipient_expires_first", "the operator could be paid and find it cannot deliver");
|
|
1431
2045
|
}
|
|
1432
2046
|
}
|
|
1433
2047
|
async function payRequestFor(listed, payment) {
|
|
@@ -1445,7 +2059,7 @@ async function reachable(url, payment) {
|
|
|
1445
2059
|
throw new Error(`${url} is not a public https URL`);
|
|
1446
2060
|
}
|
|
1447
2061
|
const response = await fetch(url, {
|
|
1448
|
-
signal: AbortSignal.timeout(
|
|
2062
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2),
|
|
1449
2063
|
headers: { accept: "application/json" }
|
|
1450
2064
|
});
|
|
1451
2065
|
if (!response.ok) {
|
|
@@ -1464,6 +2078,280 @@ function equalIgnoringCase(one, other) {
|
|
|
1464
2078
|
return one.toLowerCase() === other.toLowerCase();
|
|
1465
2079
|
}
|
|
1466
2080
|
|
|
2081
|
+
// src/request.ts
|
|
2082
|
+
function paymentRequestOf(gateway, payment, options) {
|
|
2083
|
+
return {
|
|
2084
|
+
id: payment.id,
|
|
2085
|
+
bolt11: payment.bolt11,
|
|
2086
|
+
paymentHash: payment.paymentHash,
|
|
2087
|
+
lnAddress: payment.lnAddress,
|
|
2088
|
+
amountMsat: payment.amountMsat,
|
|
2089
|
+
expiresAt: payment.expiresAt,
|
|
2090
|
+
qr: invoiceToSvg(payment.bolt11, options?.qr),
|
|
2091
|
+
payment,
|
|
2092
|
+
paid: async (waiting) => paidOnly(await gateway.settled(payment.id, { ...waitingOf(options), ...waiting })),
|
|
2093
|
+
prove: () => proveSettlement(payment, { paidTo: [payment.lnAddress], amountMsat: payment.amountMsat }),
|
|
2094
|
+
onPaid: (arrived, failed) => {
|
|
2095
|
+
const stop = new AbortController();
|
|
2096
|
+
const waiting = waitingOf(options);
|
|
2097
|
+
const signal = waiting.signal ? AbortSignal.any([stop.signal, waiting.signal]) : stop.signal;
|
|
2098
|
+
gateway.settled(payment.id, { ...waiting, signal }).then((ended) => arrived(paidOnly(ended))).catch((reason) => {
|
|
2099
|
+
if (!stop.signal.aborted) {
|
|
2100
|
+
failed?.(reason);
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
return () => stop.abort();
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function paidOnly(ended) {
|
|
2108
|
+
if (ended.status !== "paid") {
|
|
2109
|
+
throw new Error(`payment ${ended.id} ended ${ended.status} rather than paid`);
|
|
2110
|
+
}
|
|
2111
|
+
if (ended.kind !== "minted") {
|
|
2112
|
+
throw new Error(`payment ${ended.id} came back without the invoice it was minted with`);
|
|
2113
|
+
}
|
|
2114
|
+
return ended;
|
|
2115
|
+
}
|
|
2116
|
+
function waitingOf(options) {
|
|
2117
|
+
return { signal: options?.signal, tickets: options?.tickets };
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/trigger.ts
|
|
2121
|
+
var NONCE_BYTES = 16;
|
|
2122
|
+
function lnurlPayEndpoint(gateway, config) {
|
|
2123
|
+
return async (request) => {
|
|
2124
|
+
const url = new URL(request.url);
|
|
2125
|
+
const asked = url.searchParams.get("to");
|
|
2126
|
+
try {
|
|
2127
|
+
return asked === null ? await offer(gateway, config, url) : await mint(gateway, config, url, asked);
|
|
2128
|
+
} catch (failure) {
|
|
2129
|
+
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
function watchTicketEndpoint(gateway, config) {
|
|
2134
|
+
return async (request) => {
|
|
2135
|
+
const offered = await offeredSecret(request);
|
|
2136
|
+
if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
|
|
2137
|
+
return Response.json({ reason: "not the watch secret" }, { status: 403 });
|
|
2138
|
+
}
|
|
2139
|
+
return await issue(gateway, config);
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function publicWatchTicketEndpoint(gateway, config) {
|
|
2143
|
+
return () => issue(gateway, config);
|
|
2144
|
+
}
|
|
2145
|
+
async function offer(gateway, config, url) {
|
|
2146
|
+
const { least, most } = await spread(config.amount);
|
|
2147
|
+
const quote = await gateway.quote({ paidTo: config.paidTo, amount: msat(least) });
|
|
2148
|
+
const nonce = randomNonce();
|
|
2149
|
+
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
2150
|
+
callback.searchParams.set("to", quote.lnAddress);
|
|
2151
|
+
callback.searchParams.set("least", String(least));
|
|
2152
|
+
callback.searchParams.set("most", String(most));
|
|
2153
|
+
callback.searchParams.set("n", nonce);
|
|
2154
|
+
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, least, most, nonce));
|
|
2155
|
+
return Response.json({
|
|
2156
|
+
tag: "payRequest",
|
|
2157
|
+
callback: callback.toString(),
|
|
2158
|
+
metadata: quote.metadata,
|
|
2159
|
+
minSendable: least,
|
|
2160
|
+
maxSendable: most
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
async function spread(amount) {
|
|
2164
|
+
if (typeof amount === "number" || typeof amount === "function") {
|
|
2165
|
+
const pinned = await amountNow(amount);
|
|
2166
|
+
return { least: pinned, most: pinned };
|
|
2167
|
+
}
|
|
2168
|
+
const least = await amountNow(amount.least);
|
|
2169
|
+
const most = await amountNow(amount.most);
|
|
2170
|
+
if (most < least) {
|
|
2171
|
+
throw new Error(`a range cannot end at ${most} msat when it starts at ${least}`);
|
|
2172
|
+
}
|
|
2173
|
+
return { least, most };
|
|
2174
|
+
}
|
|
2175
|
+
async function mint(gateway, config, url, address) {
|
|
2176
|
+
const least = Number(url.searchParams.get("least"));
|
|
2177
|
+
const most = Number(url.searchParams.get("most"));
|
|
2178
|
+
const nonce = url.searchParams.get("n") ?? "";
|
|
2179
|
+
const signature = url.searchParams.get("sig") ?? "";
|
|
2180
|
+
const expected = await sign(config.secret, address, least, most, nonce);
|
|
2181
|
+
if (!equalInConstantTime(signature, expected)) {
|
|
2182
|
+
return refuse("this callback was not signed here");
|
|
2183
|
+
}
|
|
2184
|
+
const asked = url.searchParams.get("amount");
|
|
2185
|
+
const amountMsat = asked === null ? least : Number(asked);
|
|
2186
|
+
if (!Number.isSafeInteger(amountMsat) || amountMsat < least || amountMsat > most) {
|
|
2187
|
+
return refuse(
|
|
2188
|
+
least === most ? `this trigger costs exactly ${least} msat` : `this trigger takes between ${least} and ${most} msat`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
const minted = config.blind ? await mintBlind(gateway, config, address, amountMsat) : await mintThroughGateway(gateway, config, address, amountMsat, nonce);
|
|
2192
|
+
return Response.json({
|
|
2193
|
+
status: "OK",
|
|
2194
|
+
pr: minted.bolt11,
|
|
2195
|
+
routes: [],
|
|
2196
|
+
verify: minted.verifyUrl
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
async function mintThroughGateway(gateway, config, address, amountMsat, nonce) {
|
|
2200
|
+
const payment = await gateway.mint(
|
|
2201
|
+
{ paidTo: address, amount: msat(amountMsat) },
|
|
2202
|
+
{ idempotencyKey: nonce, trigger: config.watchSecret, replay: config.replay }
|
|
2203
|
+
);
|
|
2204
|
+
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
2205
|
+
}
|
|
2206
|
+
async function mintBlind(gateway, config, address, amountMsat) {
|
|
2207
|
+
const resolved2 = await resolve(
|
|
2208
|
+
config.send ?? pinnedToTheAddressWeVerified,
|
|
2209
|
+
[address],
|
|
2210
|
+
amountMsat
|
|
2211
|
+
);
|
|
2212
|
+
const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
|
|
2213
|
+
const locked = config.sealed;
|
|
2214
|
+
const relay = config.relayThrough;
|
|
2215
|
+
try {
|
|
2216
|
+
await gateway.watch({
|
|
2217
|
+
paymentHash: resolved2.paymentHash,
|
|
2218
|
+
verifyUrl: relay ? await relayedVerifyUrl(
|
|
2219
|
+
relay.endpoint,
|
|
2220
|
+
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
2221
|
+
relay.secret
|
|
2222
|
+
) : resolved2.verifyUrl,
|
|
2223
|
+
expiresAt: resolved2.expiresAt,
|
|
2224
|
+
trigger: config.watchSecret,
|
|
2225
|
+
replay: config.replay,
|
|
2226
|
+
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
2227
|
+
});
|
|
2228
|
+
} catch (refused) {
|
|
2229
|
+
if (!alreadyWatched(refused)) {
|
|
2230
|
+
throw refused;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
|
|
2234
|
+
}
|
|
2235
|
+
function alreadyWatched(refused) {
|
|
2236
|
+
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
2237
|
+
}
|
|
2238
|
+
function sign(secret, address, least, most, nonce) {
|
|
2239
|
+
return hmacHex(secret, `${address}|${least}|${most}|${nonce}`);
|
|
2240
|
+
}
|
|
2241
|
+
function randomNonce() {
|
|
2242
|
+
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
2243
|
+
let hex = "";
|
|
2244
|
+
for (const byte of bytes) {
|
|
2245
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
2246
|
+
}
|
|
2247
|
+
return hex;
|
|
2248
|
+
}
|
|
2249
|
+
function refuse(reason) {
|
|
2250
|
+
return Response.json({ status: "ERROR", reason });
|
|
2251
|
+
}
|
|
2252
|
+
async function issue(gateway, config) {
|
|
2253
|
+
const issued = await gateway.ticket(config.watchSecret, { replay: config.replay });
|
|
2254
|
+
return Response.json({
|
|
2255
|
+
ticket: issued.ticket,
|
|
2256
|
+
expires_at: new Date(issued.expiresAt * 1e3).toISOString()
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
async function offeredSecret(request) {
|
|
2260
|
+
const body = await request.json().catch(() => null);
|
|
2261
|
+
return typeof body?.secret === "string" ? body.secret : "";
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/serving.ts
|
|
2265
|
+
var Serve = class {
|
|
2266
|
+
constructor(gateway) {
|
|
2267
|
+
this.gateway = gateway;
|
|
2268
|
+
}
|
|
2269
|
+
gateway;
|
|
2270
|
+
/**
|
|
2271
|
+
* An LNURL-pay endpoint of your own, standing in front of a priority list of
|
|
2272
|
+
* addresses, so a printed QR points at your domain and never expires
|
|
2273
|
+
*/
|
|
2274
|
+
lnurlPay(config) {
|
|
2275
|
+
return lnurlPayEndpoint(this.gateway, config);
|
|
2276
|
+
}
|
|
2277
|
+
/** Trades the watch secret for a one minute socket ticket, refusing anyone without it */
|
|
2278
|
+
watchTicket(config) {
|
|
2279
|
+
return watchTicketEndpoint(this.gateway, config);
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Mints a socket ticket for anybody who asks, which makes the trigger's whole
|
|
2283
|
+
* stream public, preimages included. Only for a board where that is the point
|
|
2284
|
+
*/
|
|
2285
|
+
publicWatchTicket(config) {
|
|
2286
|
+
return publicWatchTicketEndpoint(this.gateway, config);
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* A verify endpoint of your own that asks the recipient's wallet for you, so
|
|
2290
|
+
* the gateway polls you and never the wallet
|
|
2291
|
+
*/
|
|
2292
|
+
verify(config) {
|
|
2293
|
+
return lightningVerifyEndpoint(config);
|
|
2294
|
+
}
|
|
2295
|
+
/** The verify endpoint a bank rail is polled at, answering off your own statement */
|
|
2296
|
+
bankVerify(config) {
|
|
2297
|
+
return bankVerifyEndpoint(config);
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* The whole webhook route: it answers the gateway's challenge, checks the
|
|
2301
|
+
* signature against the key the gateway publishes, refuses a settlement that
|
|
2302
|
+
* proves nothing, and calls you for the one that does.
|
|
2303
|
+
*
|
|
2304
|
+
* `export const POST = gateway.serve.webhook({ onSettled: fulfil })` is the
|
|
2305
|
+
* entire integration
|
|
2306
|
+
*/
|
|
2307
|
+
webhook(handlers) {
|
|
2308
|
+
const options = { toleranceSecs: handlers.toleranceSecs };
|
|
2309
|
+
return async (request) => {
|
|
2310
|
+
const credential = await this.credential(handlers);
|
|
2311
|
+
const challenge = await answerWebhookChallenge(request, credential, options);
|
|
2312
|
+
if (challenge !== null) {
|
|
2313
|
+
return challenge;
|
|
2314
|
+
}
|
|
2315
|
+
const settlement = await readSettlement(request, credential, options);
|
|
2316
|
+
if (settlement !== null) {
|
|
2317
|
+
return await acted(settlement, handlers);
|
|
2318
|
+
}
|
|
2319
|
+
const payment = await readPayment(request, credential, options);
|
|
2320
|
+
if (payment !== null) {
|
|
2321
|
+
await handlers.onPayment?.(payment);
|
|
2322
|
+
return new Response("ok");
|
|
2323
|
+
}
|
|
2324
|
+
return new Response("bad signature", { status: 401 });
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
/** Verify a delivery and read the settlement out of it, null when it is not believable */
|
|
2328
|
+
async readSettlement(request, options) {
|
|
2329
|
+
return await readSettlement(request, await this.credential({}), options);
|
|
2330
|
+
}
|
|
2331
|
+
/** Verify a delivery and read the payment out of it, null when it is not believable */
|
|
2332
|
+
async readPayment(request, options) {
|
|
2333
|
+
return await readPayment(request, await this.credential({}), options);
|
|
2334
|
+
}
|
|
2335
|
+
/** Answer the challenge the gateway sends before it will post to a webhook of yours */
|
|
2336
|
+
async answerWebhookChallenge(request, options) {
|
|
2337
|
+
return await answerWebhookChallenge(request, await this.credential({}), options);
|
|
2338
|
+
}
|
|
2339
|
+
async credential(handlers) {
|
|
2340
|
+
return handlers.credential ?? { publicKey: await this.gateway.webhookKey() };
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
async function acted(settlement, handlers) {
|
|
2344
|
+
if (carriesProof(settlement)) {
|
|
2345
|
+
await handlers.onSettled?.(settlement);
|
|
2346
|
+
return new Response("ok");
|
|
2347
|
+
}
|
|
2348
|
+
if (handlers.onUnproven === void 0) {
|
|
2349
|
+
return new Response("the recipient released no preimage", { status: 202 });
|
|
2350
|
+
}
|
|
2351
|
+
await handlers.onUnproven(settlement);
|
|
2352
|
+
return new Response("ok");
|
|
2353
|
+
}
|
|
2354
|
+
|
|
1467
2355
|
// src/client.ts
|
|
1468
2356
|
var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
|
|
1469
2357
|
var RECONNECT_DELAY_MS = 3e3;
|
|
@@ -1483,6 +2371,13 @@ function backoffMs(firstDelay, attempt) {
|
|
|
1483
2371
|
const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
|
|
1484
2372
|
return Math.round(grown * (0.5 + Math.random() / 2));
|
|
1485
2373
|
}
|
|
2374
|
+
function gatewayAt(baseUrl) {
|
|
2375
|
+
const at = URL.canParse(baseUrl) ? new URL(baseUrl) : null;
|
|
2376
|
+
if (at === null || at.protocol !== "http:" && at.protocol !== "https:") {
|
|
2377
|
+
throw new Error(`${baseUrl} is not an http or https gateway url`);
|
|
2378
|
+
}
|
|
2379
|
+
return baseUrl.replace(/\/+$/, "");
|
|
2380
|
+
}
|
|
1486
2381
|
var ThunderBridge = class {
|
|
1487
2382
|
baseUrl;
|
|
1488
2383
|
verify;
|
|
@@ -1490,28 +2385,34 @@ var ThunderBridge = class {
|
|
|
1490
2385
|
secret;
|
|
1491
2386
|
strangers = null;
|
|
1492
2387
|
speaks = null;
|
|
2388
|
+
published = null;
|
|
2389
|
+
/** Everything this gateway lets you mount, from an LNURL endpoint to a webhook route */
|
|
2390
|
+
serve;
|
|
2391
|
+
/** One call per sale, whatever the rail moves */
|
|
2392
|
+
rails;
|
|
1493
2393
|
constructor(baseUrl, options) {
|
|
1494
|
-
this.baseUrl = baseUrl
|
|
2394
|
+
this.baseUrl = gatewayAt(baseUrl);
|
|
1495
2395
|
this.verify = options?.verify ?? true;
|
|
1496
2396
|
this.token = options?.token ?? null;
|
|
1497
2397
|
this.secret = options?.secret ?? null;
|
|
2398
|
+
this.serve = new Serve(this);
|
|
2399
|
+
this.rails = new Rails(this);
|
|
1498
2400
|
}
|
|
1499
2401
|
/**
|
|
1500
|
-
* Whether a token was given, which is
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
2402
|
+
* Whether a token was given to this instance, which is your side of the
|
|
2403
|
+
* arrangement and says nothing about the gateway's. `refusesStrangers` is the
|
|
2404
|
+
* one that asks the gateway, and it is the one to guard anything with
|
|
1503
2405
|
*/
|
|
1504
|
-
get
|
|
2406
|
+
get hasToken() {
|
|
1505
2407
|
return this.token !== null;
|
|
1506
2408
|
}
|
|
1507
2409
|
/**
|
|
1508
2410
|
* Whether the gateway turns away a caller carrying no token, asked by making
|
|
1509
|
-
* one unauthenticated read it would have to refuse. `
|
|
1510
|
-
* whether you configured
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
* open, so an unreachable gateway fails closed
|
|
2411
|
+
* one unauthenticated read it would have to refuse. `hasToken` answers only
|
|
2412
|
+
* whether you configured one, so a made-up token against a public instance
|
|
2413
|
+
* reads as yours and is not. Asked once and remembered, because an instance
|
|
2414
|
+
* does not change its mind. Anything other than a refusal counts as open, so
|
|
2415
|
+
* an unreachable gateway fails closed
|
|
1515
2416
|
*/
|
|
1516
2417
|
async refusesStrangers() {
|
|
1517
2418
|
this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
|
|
@@ -1519,14 +2420,24 @@ var ThunderBridge = class {
|
|
|
1519
2420
|
}).then((answer) => answer.status === 401).catch(() => false);
|
|
1520
2421
|
return await this.strangers;
|
|
1521
2422
|
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Ask to be paid for one thing. It mints the invoice, proves it came from the
|
|
2425
|
+
* address you asked for, draws the QR and hands back one object with a way to
|
|
2426
|
+
* wait for the money. This is `mint` plus the two things every caller does next
|
|
2427
|
+
*/
|
|
2428
|
+
async requestPayment(asked) {
|
|
2429
|
+
return paymentRequestOf(this, await this.mint(asked, asked), asked);
|
|
2430
|
+
}
|
|
1522
2431
|
/**
|
|
1523
2432
|
* Ask the gateway for an invoice payable to the first address on your list
|
|
1524
2433
|
* that can issue a provable one, throws `NoWalletAvailableError` when none can
|
|
1525
2434
|
* and `GatewayCheatError` when what comes back is not what you asked for
|
|
1526
2435
|
*/
|
|
1527
|
-
async
|
|
2436
|
+
async mint(charge, options) {
|
|
2437
|
+
const asked = await priced(charge);
|
|
1528
2438
|
const sent = createRequestBody(
|
|
1529
|
-
|
|
2439
|
+
asked,
|
|
2440
|
+
charge.webhookUrl,
|
|
1530
2441
|
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null,
|
|
1531
2442
|
options?.replay
|
|
1532
2443
|
);
|
|
@@ -1542,9 +2453,9 @@ var ThunderBridge = class {
|
|
|
1542
2453
|
if (!response.ok) {
|
|
1543
2454
|
throw await problemFrom(response);
|
|
1544
2455
|
}
|
|
1545
|
-
const payment = await
|
|
2456
|
+
const payment = await mintedFrom(response);
|
|
1546
2457
|
if (this.verify) {
|
|
1547
|
-
await proveOrigin(payment,
|
|
2458
|
+
await proveOrigin(payment, asked);
|
|
1548
2459
|
}
|
|
1549
2460
|
return payment;
|
|
1550
2461
|
}
|
|
@@ -1555,8 +2466,8 @@ var ThunderBridge = class {
|
|
|
1555
2466
|
* whether a wallet returns a provable invoice cannot be known without asking
|
|
1556
2467
|
* it for one, and asking mints it
|
|
1557
2468
|
*/
|
|
1558
|
-
async
|
|
1559
|
-
const sent = quoteRequestBody(
|
|
2469
|
+
async quote(charge) {
|
|
2470
|
+
const sent = quoteRequestBody(await priced(charge));
|
|
1560
2471
|
const response = await fetch(`${this.baseUrl}/quotes`, {
|
|
1561
2472
|
method: "POST",
|
|
1562
2473
|
headers: await this.sending("/quotes", sent),
|
|
@@ -1574,13 +2485,19 @@ var ThunderBridge = class {
|
|
|
1574
2485
|
}
|
|
1575
2486
|
return quote;
|
|
1576
2487
|
}
|
|
1577
|
-
/** Read a payment back, null when the gateway has never heard of it */
|
|
1578
2488
|
/**
|
|
1579
|
-
* The key this gateway signs webhooks with when you registered none of your own
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
2489
|
+
* The key this gateway signs webhooks with when you registered none of your own.
|
|
2490
|
+
* `serve.webhook` reads it for you. Asked once and kept, because it is the same
|
|
2491
|
+
* for every instance in the cluster
|
|
1582
2492
|
*/
|
|
1583
2493
|
async webhookKey() {
|
|
2494
|
+
this.published ??= this.publishedKey().catch((refused) => {
|
|
2495
|
+
this.published = null;
|
|
2496
|
+
throw refused;
|
|
2497
|
+
});
|
|
2498
|
+
return await this.published;
|
|
2499
|
+
}
|
|
2500
|
+
async publishedKey() {
|
|
1584
2501
|
const response = await fetch(`${this.baseUrl}/webhook-key`);
|
|
1585
2502
|
if (!response.ok) {
|
|
1586
2503
|
throw await problemFrom(response);
|
|
@@ -1594,25 +2511,12 @@ var ThunderBridge = class {
|
|
|
1594
2511
|
}
|
|
1595
2512
|
return body.public_key;
|
|
1596
2513
|
}
|
|
1597
|
-
async getPayment(id) {
|
|
1598
|
-
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
1599
|
-
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1600
|
-
headers: await this.reading("GET", path)
|
|
1601
|
-
});
|
|
1602
|
-
if (response.status === 404) {
|
|
1603
|
-
return null;
|
|
1604
|
-
}
|
|
1605
|
-
if (!response.ok) {
|
|
1606
|
-
throw await problemFrom(response);
|
|
1607
|
-
}
|
|
1608
|
-
return this.checked(await paymentFrom(response));
|
|
1609
|
-
}
|
|
1610
2514
|
/**
|
|
1611
|
-
* Read
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
2515
|
+
* Read a payment back, null when the gateway has never heard of it. One method
|
|
2516
|
+
* for both sorts: `kind` says whether the gateway minted it or was handed it,
|
|
2517
|
+
* and the address, amount and invoice are null on one it was never told
|
|
1614
2518
|
*/
|
|
1615
|
-
async
|
|
2519
|
+
async payment(id) {
|
|
1616
2520
|
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
1617
2521
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1618
2522
|
headers: await this.reading("GET", path)
|
|
@@ -1620,17 +2524,10 @@ var ThunderBridge = class {
|
|
|
1620
2524
|
if (response.status === 404) {
|
|
1621
2525
|
return null;
|
|
1622
2526
|
}
|
|
1623
|
-
if (!response.ok) {
|
|
1624
|
-
throw await problemFrom(response);
|
|
1625
|
-
}
|
|
1626
|
-
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
1627
|
-
if (watched === null) {
|
|
1628
|
-
throw new ProblemError({
|
|
1629
|
-
status: response.status,
|
|
1630
|
-
title: "The gateway answered with something that is not a payment"
|
|
1631
|
-
});
|
|
2527
|
+
if (!response.ok) {
|
|
2528
|
+
throw await problemFrom(response);
|
|
1632
2529
|
}
|
|
1633
|
-
return this.proven(
|
|
2530
|
+
return this.proven(await paymentFrom(response));
|
|
1634
2531
|
}
|
|
1635
2532
|
/**
|
|
1636
2533
|
* List what this gateway is watching, newest first. Only a gateway started
|
|
@@ -1641,7 +2538,7 @@ var ThunderBridge = class {
|
|
|
1641
2538
|
* Anything older than that window is not in the answer, and the list does not
|
|
1642
2539
|
* pretend otherwise
|
|
1643
2540
|
*/
|
|
1644
|
-
async
|
|
2541
|
+
async payments(limit) {
|
|
1645
2542
|
const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
|
|
1646
2543
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1647
2544
|
headers: await this.reading("GET", path)
|
|
@@ -1650,7 +2547,7 @@ var ThunderBridge = class {
|
|
|
1650
2547
|
throw await problemFrom(response);
|
|
1651
2548
|
}
|
|
1652
2549
|
const body = await response.json().catch(() => null);
|
|
1653
|
-
const listed = Array.isArray(body?.payments) ? body.payments.map(
|
|
2550
|
+
const listed = Array.isArray(body?.payments) ? body.payments.map(paymentFromWire).filter((one) => one !== null) : null;
|
|
1654
2551
|
if (listed === null || typeof body?.settled_scanned !== "number") {
|
|
1655
2552
|
throw new ProblemError({
|
|
1656
2553
|
status: response.status,
|
|
@@ -1665,34 +2562,15 @@ var ThunderBridge = class {
|
|
|
1665
2562
|
* one that has answered is followed until its own expiry, so the wait always
|
|
1666
2563
|
* ends by itself
|
|
1667
2564
|
*/
|
|
1668
|
-
async
|
|
1669
|
-
const
|
|
1670
|
-
if (
|
|
1671
|
-
throw new ProblemError({
|
|
1672
|
-
status: 200,
|
|
1673
|
-
title: `payment ${id} is watched rather than minted here, read it with waitForWatched`
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
return this.checked(payment);
|
|
1677
|
-
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Follow a payment the gateway is only watching, one it did not mint, until it
|
|
1680
|
-
* is paid or expired.
|
|
1681
|
-
*
|
|
1682
|
-
* A watched payment carries no address, no amount and no invoice, because the
|
|
1683
|
-
* gateway was told none of them, so it reads back as the shape a trigger
|
|
1684
|
-
* streams rather than as a `Payment`. That is every bank transfer, and every
|
|
1685
|
-
* Lightning invoice registered with `watchPayment` instead of `createPayment`.
|
|
1686
|
-
*/
|
|
1687
|
-
async waitForWatched(id, options) {
|
|
1688
|
-
const watched = triggerEventFromWire(await this.followed(id, options));
|
|
1689
|
-
if (watched === null) {
|
|
2565
|
+
async settled(id, options) {
|
|
2566
|
+
const ended = paymentFromWire(await this.followed(id, options));
|
|
2567
|
+
if (ended === null) {
|
|
1690
2568
|
throw new ProblemError({
|
|
1691
2569
|
status: 200,
|
|
1692
2570
|
title: "The gateway answered with something that is not a payment"
|
|
1693
2571
|
});
|
|
1694
2572
|
}
|
|
1695
|
-
return this.proven(
|
|
2573
|
+
return this.proven(ended);
|
|
1696
2574
|
}
|
|
1697
2575
|
followed(id, options) {
|
|
1698
2576
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
@@ -1759,7 +2637,7 @@ var ThunderBridge = class {
|
|
|
1759
2637
|
opened.onmessage = (event) => {
|
|
1760
2638
|
try {
|
|
1761
2639
|
const frame = JSON.parse(String(event.data));
|
|
1762
|
-
const watched =
|
|
2640
|
+
const watched = paymentFromWire(frame);
|
|
1763
2641
|
if (watched === null) {
|
|
1764
2642
|
return;
|
|
1765
2643
|
}
|
|
@@ -1788,16 +2666,16 @@ var ThunderBridge = class {
|
|
|
1788
2666
|
* what you want is the one that arrives and nothing further from the other.
|
|
1789
2667
|
*
|
|
1790
2668
|
* A leg that expires is a loser, not a winner, which is the whole reason this is
|
|
1791
|
-
* not a race: `
|
|
2669
|
+
* not a race: `settled` ends on `paid` and on `expired` alike, and a
|
|
1792
2670
|
* Lightning invoice expires in an hour while a bank transfer takes days. `null`
|
|
1793
2671
|
* means every leg ended without being paid.
|
|
1794
2672
|
*
|
|
1795
2673
|
* Stopping the wait is not revoking the invoice. Nobody can revoke one, because
|
|
1796
2674
|
* the recipient's own wallet minted it, so a payer who pays the loser afterwards
|
|
1797
|
-
* really does pay twice and that shows up on `
|
|
1798
|
-
*
|
|
2675
|
+
* really does pay twice and that shows up on `follow` as a second settlement to
|
|
2676
|
+
* refund.
|
|
1799
2677
|
*/
|
|
1800
|
-
async
|
|
2678
|
+
async firstSettled(ids, options) {
|
|
1801
2679
|
if (ids.length === 0) {
|
|
1802
2680
|
return null;
|
|
1803
2681
|
}
|
|
@@ -1814,7 +2692,7 @@ var ThunderBridge = class {
|
|
|
1814
2692
|
}
|
|
1815
2693
|
};
|
|
1816
2694
|
for (const id of ids) {
|
|
1817
|
-
this.
|
|
2695
|
+
this.settled(id, { ...options, signal }).then((watched) => watched.status === "paid" ? resolve2(watched) : lost()).catch((failure) => {
|
|
1818
2696
|
refused ??= failure;
|
|
1819
2697
|
lost();
|
|
1820
2698
|
});
|
|
@@ -1834,10 +2712,10 @@ var ThunderBridge = class {
|
|
|
1834
2712
|
* rather than one recipient, which is what makes leaving it cheap. Anything
|
|
1835
2713
|
* the watcher needs goes in `sealed`, which the gateway cannot read
|
|
1836
2714
|
*/
|
|
1837
|
-
async
|
|
2715
|
+
async watch(handover) {
|
|
1838
2716
|
const sent = watchRequestBody(
|
|
1839
|
-
|
|
1840
|
-
|
|
2717
|
+
handover,
|
|
2718
|
+
handover.trigger ? sha256Hex(unguessable(handover.trigger)) : null
|
|
1841
2719
|
);
|
|
1842
2720
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
1843
2721
|
method: "POST",
|
|
@@ -1847,14 +2725,14 @@ var ThunderBridge = class {
|
|
|
1847
2725
|
if (!response.ok) {
|
|
1848
2726
|
throw await problemFrom(response);
|
|
1849
2727
|
}
|
|
1850
|
-
const watched =
|
|
2728
|
+
const watched = paymentFromWire(await response.json().catch(() => null));
|
|
1851
2729
|
if (watched === null) {
|
|
1852
2730
|
throw new ProblemError({
|
|
1853
2731
|
status: response.status,
|
|
1854
2732
|
title: "The gateway answered with something that is not a watched payment"
|
|
1855
2733
|
});
|
|
1856
2734
|
}
|
|
1857
|
-
const named = await this.nameFor(
|
|
2735
|
+
const named = await this.nameFor(handover.paymentHash);
|
|
1858
2736
|
if (named !== null && watched.id !== named) {
|
|
1859
2737
|
throw new GatewayCheatError("id_not_mine", watched.id);
|
|
1860
2738
|
}
|
|
@@ -1875,7 +2753,7 @@ var ThunderBridge = class {
|
|
|
1875
2753
|
* connect and then live, reconnecting on its own until the returned function
|
|
1876
2754
|
* is called. A trigger has no terminal state, so this never resolves
|
|
1877
2755
|
*/
|
|
1878
|
-
|
|
2756
|
+
follow(secret, options) {
|
|
1879
2757
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
1880
2758
|
const asked = options.replay === void 0 ? "" : `?replay=${options.replay}`;
|
|
1881
2759
|
const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}${asked}`;
|
|
@@ -1914,7 +2792,7 @@ var ThunderBridge = class {
|
|
|
1914
2792
|
};
|
|
1915
2793
|
socket.onmessage = (event) => {
|
|
1916
2794
|
try {
|
|
1917
|
-
const settled =
|
|
2795
|
+
const settled = paymentFromWire(JSON.parse(String(event.data)));
|
|
1918
2796
|
if (settled !== null) {
|
|
1919
2797
|
options.onPayment(this.proven(settled));
|
|
1920
2798
|
}
|
|
@@ -1936,13 +2814,13 @@ var ThunderBridge = class {
|
|
|
1936
2814
|
* A one minute pass onto one trigger's stream, for something that must hold
|
|
1937
2815
|
* neither the token nor the trigger secret. Mint it in a handler and answer
|
|
1938
2816
|
* with the ticket alone, because that is all a browser needs to connect and
|
|
1939
|
-
* all it can do anything with. `
|
|
2817
|
+
* all it can do anything with. `serve.watchTicket` is this method already
|
|
1940
2818
|
* wrapped in a route
|
|
1941
2819
|
*/
|
|
1942
|
-
async
|
|
2820
|
+
async ticket(trigger, options) {
|
|
1943
2821
|
return await this.mintedTicket({
|
|
1944
|
-
trigger_secret: unguessable(
|
|
1945
|
-
replay:
|
|
2822
|
+
trigger_secret: unguessable(trigger),
|
|
2823
|
+
replay: options?.replay
|
|
1946
2824
|
});
|
|
1947
2825
|
}
|
|
1948
2826
|
needsTicket(asked) {
|
|
@@ -1983,15 +2861,8 @@ var ThunderBridge = class {
|
|
|
1983
2861
|
this.speaks ??= callerKey(this.secret ?? "");
|
|
1984
2862
|
return await this.speaks;
|
|
1985
2863
|
}
|
|
1986
|
-
proven(
|
|
1987
|
-
|
|
1988
|
-
if (this.verify && unproven) {
|
|
1989
|
-
throw new GatewayCheatError("preimage_mismatch", settled.id);
|
|
1990
|
-
}
|
|
1991
|
-
return settled;
|
|
1992
|
-
}
|
|
1993
|
-
checked(payment) {
|
|
1994
|
-
if (this.verify && payment.status === "paid" && !isProvablyPaid(payment)) {
|
|
2864
|
+
proven(payment) {
|
|
2865
|
+
if (this.verify && payment.status === "paid" && !carriesProof(payment)) {
|
|
1995
2866
|
throw new GatewayCheatError("preimage_mismatch", payment.id);
|
|
1996
2867
|
}
|
|
1997
2868
|
return payment;
|
|
@@ -2007,11 +2878,21 @@ async function paymentFrom(response) {
|
|
|
2007
2878
|
}
|
|
2008
2879
|
return payment;
|
|
2009
2880
|
}
|
|
2881
|
+
async function mintedFrom(response) {
|
|
2882
|
+
const payment = mintedFromWire(await response.json().catch(() => null));
|
|
2883
|
+
if (payment === null) {
|
|
2884
|
+
throw new ProblemError({
|
|
2885
|
+
status: response.status,
|
|
2886
|
+
title: "The gateway answered with something that is not a minted payment"
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
return payment;
|
|
2890
|
+
}
|
|
2010
2891
|
async function problemFrom(response) {
|
|
2011
2892
|
const problem = await response.json().catch(() => ({}));
|
|
2012
2893
|
const document = { ...problem, status: response.status };
|
|
2013
2894
|
if (isProblemType(problem, NO_WALLET_AVAILABLE)) {
|
|
2014
|
-
return new NoWalletAvailableError(document,
|
|
2895
|
+
return new NoWalletAvailableError(document, refusals2(problem.wallets));
|
|
2015
2896
|
}
|
|
2016
2897
|
if (isProblemType(problem, REQUEST_IN_FLIGHT)) {
|
|
2017
2898
|
return new IdempotencyConflictError(document, "request-in-flight");
|
|
@@ -2021,7 +2902,7 @@ async function problemFrom(response) {
|
|
|
2021
2902
|
}
|
|
2022
2903
|
return new ProblemError(document);
|
|
2023
2904
|
}
|
|
2024
|
-
function
|
|
2905
|
+
function refusals2(wallets) {
|
|
2025
2906
|
if (!Array.isArray(wallets)) {
|
|
2026
2907
|
return [];
|
|
2027
2908
|
}
|
|
@@ -2030,118 +2911,6 @@ function refusals(wallets) {
|
|
|
2030
2911
|
});
|
|
2031
2912
|
}
|
|
2032
2913
|
|
|
2033
|
-
// src/fio.ts
|
|
2034
|
-
var BASE_URL = "https://fioapi.fio.cz/v1/rest";
|
|
2035
|
-
var MINOR_UNITS = 100;
|
|
2036
|
-
var MILLIS = 1e3;
|
|
2037
|
-
var DATE_CHARS = "2026-08-04".length;
|
|
2038
|
-
var DEFAULT_MIN_INTERVAL_SECS = 30;
|
|
2039
|
-
var TOO_SOON = 409;
|
|
2040
|
-
var BOOKED_AT = "column0";
|
|
2041
|
-
var AMOUNT = "column1";
|
|
2042
|
-
var CURRENCY = "column14";
|
|
2043
|
-
var VARIABLE_SYMBOL = "column5";
|
|
2044
|
-
var USER_IDENTIFICATION = "column7";
|
|
2045
|
-
var MESSAGE_FOR_RECIPIENT = "column16";
|
|
2046
|
-
var PAYER_REFERENCE = "column27";
|
|
2047
|
-
var WHERE_A_PAYER_WRITES = [
|
|
2048
|
-
MESSAGE_FOR_RECIPIENT,
|
|
2049
|
-
VARIABLE_SYMBOL,
|
|
2050
|
-
USER_IDENTIFICATION,
|
|
2051
|
-
PAYER_REFERENCE
|
|
2052
|
-
];
|
|
2053
|
-
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
2054
|
-
function fioStatement(config) {
|
|
2055
|
-
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
2056
|
-
if (named.length === 0) {
|
|
2057
|
-
throw new Error("fioStatement needs at least one token to read with");
|
|
2058
|
-
}
|
|
2059
|
-
const usedAt = new Map(named.map((token) => [token, Number.NEGATIVE_INFINITY]));
|
|
2060
|
-
const tokenWindowMs = (config.minIntervalSecs ?? DEFAULT_MIN_INTERVAL_SECS) * MILLIS;
|
|
2061
|
-
const paceMs = tokenWindowMs / usedAt.size;
|
|
2062
|
-
let lastRead = Number.NEGATIVE_INFINITY;
|
|
2063
|
-
let credits = [];
|
|
2064
|
-
return async (sinceUnix) => {
|
|
2065
|
-
const now = Date.now();
|
|
2066
|
-
if (now - lastRead < paceMs) {
|
|
2067
|
-
return credits;
|
|
2068
|
-
}
|
|
2069
|
-
const idlest = longestUnused(usedAt);
|
|
2070
|
-
if (now - idlest.usedAt < tokenWindowMs) {
|
|
2071
|
-
return credits;
|
|
2072
|
-
}
|
|
2073
|
-
usedAt.set(idlest.token, now);
|
|
2074
|
-
lastRead = now;
|
|
2075
|
-
const url = [
|
|
2076
|
-
config.baseUrl ?? BASE_URL,
|
|
2077
|
-
"periods",
|
|
2078
|
-
idlest.token,
|
|
2079
|
-
asDate(sinceUnix),
|
|
2080
|
-
asDate(Math.floor(now / MILLIS)),
|
|
2081
|
-
"transactions.json"
|
|
2082
|
-
].join("/");
|
|
2083
|
-
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
2084
|
-
if (answer.status === TOO_SOON) {
|
|
2085
|
-
throw new Error("fio refuses a second read of this token inside its 30 second window");
|
|
2086
|
-
}
|
|
2087
|
-
if (!answer.ok) {
|
|
2088
|
-
throw new Error(`fio answered ${answer.status} reading the statement`);
|
|
2089
|
-
}
|
|
2090
|
-
const read = await answer.json();
|
|
2091
|
-
credits = (read.accountStatement?.transactionList?.transaction ?? []).filter(isCredit).map(asCredit);
|
|
2092
|
-
return credits;
|
|
2093
|
-
};
|
|
2094
|
-
}
|
|
2095
|
-
function longestUnused(usedAt) {
|
|
2096
|
-
let token = "";
|
|
2097
|
-
let idleSince = Number.POSITIVE_INFINITY;
|
|
2098
|
-
for (const [candidate, at] of usedAt) {
|
|
2099
|
-
if (at < idleSince) {
|
|
2100
|
-
token = candidate;
|
|
2101
|
-
idleSince = at;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
return { token, usedAt: idleSince };
|
|
2105
|
-
}
|
|
2106
|
-
function isCredit(transaction) {
|
|
2107
|
-
return numberIn(transaction[AMOUNT]) > 0;
|
|
2108
|
-
}
|
|
2109
|
-
function asCredit(transaction) {
|
|
2110
|
-
return {
|
|
2111
|
-
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
2112
|
-
currency: textIn(transaction[CURRENCY]),
|
|
2113
|
-
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
2114
|
-
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
function bookedAtIn(cell) {
|
|
2118
|
-
const value = cell?.value;
|
|
2119
|
-
if (typeof value === "number") {
|
|
2120
|
-
return Math.floor(value / MILLIS);
|
|
2121
|
-
}
|
|
2122
|
-
const at = Date.parse(asIso8601(textIn(cell)));
|
|
2123
|
-
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
2124
|
-
}
|
|
2125
|
-
function asIso8601(booked) {
|
|
2126
|
-
const day = booked.slice(0, DATE_CHARS);
|
|
2127
|
-
const zone = booked.slice(DATE_CHARS);
|
|
2128
|
-
if (!UTC_OFFSET.test(zone)) {
|
|
2129
|
-
return `${day}T00:00:00Z`;
|
|
2130
|
-
}
|
|
2131
|
-
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
2132
|
-
}
|
|
2133
|
-
function numberIn(cell) {
|
|
2134
|
-
const value = Number(cell?.value ?? Number.NaN);
|
|
2135
|
-
return Number.isFinite(value) ? value : 0;
|
|
2136
|
-
}
|
|
2137
|
-
function textIn(cell) {
|
|
2138
|
-
const value = cell?.value;
|
|
2139
|
-
return value === null || value === void 0 ? "" : String(value).trim();
|
|
2140
|
-
}
|
|
2141
|
-
function asDate(unix) {
|
|
2142
|
-
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
2914
|
// src/gateways.ts
|
|
2146
2915
|
var Gateways = class {
|
|
2147
2916
|
each;
|
|
@@ -2165,10 +2934,8 @@ var Gateways = class {
|
|
|
2165
2934
|
* carrying the first refusal, because one gateway that agreed is enough to be
|
|
2166
2935
|
* watched
|
|
2167
2936
|
*/
|
|
2168
|
-
async
|
|
2169
|
-
const asked = await Promise.allSettled(
|
|
2170
|
-
this.each.map((gateway) => gateway.watchPayment(params))
|
|
2171
|
-
);
|
|
2937
|
+
async watch(handover) {
|
|
2938
|
+
const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watch(handover)));
|
|
2172
2939
|
const taken = [];
|
|
2173
2940
|
const refusals3 = [];
|
|
2174
2941
|
for (const [at, answer] of asked.entries()) {
|
|
@@ -2190,7 +2957,7 @@ var Gateways = class {
|
|
|
2190
2957
|
* saying so. When they all end without a payment, the first ending is the answer,
|
|
2191
2958
|
* and when they all fail, the first failure is thrown
|
|
2192
2959
|
*/
|
|
2193
|
-
async
|
|
2960
|
+
async settled(id, options) {
|
|
2194
2961
|
const stopLosers = new AbortController();
|
|
2195
2962
|
const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
|
|
2196
2963
|
const unpaid = [];
|
|
@@ -2205,7 +2972,7 @@ var Gateways = class {
|
|
|
2205
2972
|
}
|
|
2206
2973
|
};
|
|
2207
2974
|
for (const gateway of this.each) {
|
|
2208
|
-
gateway.
|
|
2975
|
+
gateway.settled(id, { ...options, signal }).then((watched) => {
|
|
2209
2976
|
if (watched.status === "paid") {
|
|
2210
2977
|
return resolve2(watched);
|
|
2211
2978
|
}
|
|
@@ -2229,315 +2996,30 @@ var Gateways = class {
|
|
|
2229
2996
|
}
|
|
2230
2997
|
}
|
|
2231
2998
|
};
|
|
2232
|
-
|
|
2233
|
-
// src/price.ts
|
|
2234
|
-
var MSAT_PER_BTC2 = 1e11;
|
|
2235
|
-
var BASIS_POINTS = 1e4;
|
|
2236
|
-
var DEFAULT_HOLD_SECS = 60;
|
|
2237
|
-
var DEFAULT_MIN_VENUES = 2;
|
|
2238
|
-
var DEFAULT_MAX_SPREAD_BPS = 200;
|
|
2239
|
-
var MILLIS2 = 1e3;
|
|
2240
|
-
function coinbase(baseUrl = "https://api.coinbase.com") {
|
|
2241
|
-
return async (currency) => {
|
|
2242
|
-
const asked = currency.toUpperCase();
|
|
2243
|
-
const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
|
|
2244
|
-
const quoted = answer.data;
|
|
2245
|
-
if (quoted?.currency !== asked) {
|
|
2246
|
-
throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
|
|
2247
|
-
}
|
|
2248
|
-
return asMinor(quoted.amount, "coinbase", asked);
|
|
2249
|
-
};
|
|
2250
|
-
}
|
|
2251
|
-
function kraken(baseUrl = "https://api.kraken.com") {
|
|
2252
|
-
return async (currency) => {
|
|
2253
|
-
const asked = currency.toUpperCase();
|
|
2254
|
-
const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
|
|
2255
|
-
if (answer.error?.length) {
|
|
2256
|
-
throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
|
|
2257
|
-
}
|
|
2258
|
-
const pairs = Object.values(answer.result ?? {});
|
|
2259
|
-
if (pairs.length !== 1) {
|
|
2260
|
-
throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
|
|
2261
|
-
}
|
|
2262
|
-
return asMinor(pairs[0]?.c?.[0], "kraken", asked);
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
|
-
function bitstamp(baseUrl = "https://www.bitstamp.net") {
|
|
2266
|
-
return async (currency) => {
|
|
2267
|
-
const pair = `btc${currency.toLowerCase()}`;
|
|
2268
|
-
const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
|
|
2269
|
-
if (Array.isArray(answer)) {
|
|
2270
|
-
throw new Error(`bitstamp has no ${pair} pair`);
|
|
2271
|
-
}
|
|
2272
|
-
return asMinor(answer.last, "bitstamp", currency);
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
function coinmate(baseUrl = "https://coinmate.io") {
|
|
2276
|
-
return async (currency) => {
|
|
2277
|
-
const pair = `BTC_${currency.toUpperCase()}`;
|
|
2278
|
-
const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
|
|
2279
|
-
if (answer.error !== false) {
|
|
2280
|
-
throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
|
|
2281
|
-
}
|
|
2282
|
-
return asMinor(answer.data?.last, "coinmate", currency);
|
|
2283
|
-
};
|
|
2284
|
-
}
|
|
2285
|
-
function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
|
|
2286
|
-
const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS2;
|
|
2287
|
-
const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
|
|
2288
|
-
const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
|
|
2289
|
-
const held = /* @__PURE__ */ new Map();
|
|
2290
|
-
return async (currency) => {
|
|
2291
|
-
const asked = currency.toUpperCase();
|
|
2292
|
-
const remembered = held.get(asked);
|
|
2293
|
-
if (remembered && Date.now() - remembered.at < holdFor) {
|
|
2294
|
-
return remembered.price;
|
|
2295
|
-
}
|
|
2296
|
-
const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
|
|
2297
|
-
const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
|
|
2298
|
-
if (quoted.length < minVenues) {
|
|
2299
|
-
throw new Error(
|
|
2300
|
-
`${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals2(answers)}`
|
|
2301
|
-
);
|
|
2302
|
-
}
|
|
2303
|
-
const spread = spreadBpsOf(quoted);
|
|
2304
|
-
if (spread > maxSpread) {
|
|
2305
|
-
throw new Error(
|
|
2306
|
-
`venues disagree on ${asked} by ${Math.round(spread)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
|
|
2307
|
-
);
|
|
2308
|
-
}
|
|
2309
|
-
const price = middleOf(quoted);
|
|
2310
|
-
held.set(asked, { at: Date.now(), price });
|
|
2311
|
-
return price;
|
|
2312
|
-
};
|
|
2313
|
-
}
|
|
2314
|
-
function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
|
|
2315
|
-
if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
|
|
2316
|
-
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
2317
|
-
}
|
|
2318
|
-
if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
|
|
2319
|
-
throw new Error(`${priceMinorPerBtc} is not a usable price`);
|
|
2320
|
-
}
|
|
2321
|
-
const spreadBps = options.spreadBps ?? 0;
|
|
2322
|
-
if (!Number.isInteger(spreadBps) || spreadBps < 0) {
|
|
2323
|
-
throw new Error("spreadBps must be a whole number of basis points, none or more");
|
|
2324
|
-
}
|
|
2325
|
-
const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
|
|
2326
|
-
const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
|
|
2327
|
-
const msat = (wanted + per - 1n) / per;
|
|
2328
|
-
if (msat > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
2329
|
-
throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
|
|
2330
|
-
}
|
|
2331
|
-
return Number(msat);
|
|
2332
|
-
}
|
|
2333
|
-
async function asJson(url, venue) {
|
|
2334
|
-
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
2335
|
-
if (!answer.ok) {
|
|
2336
|
-
throw new Error(`${venue} answered ${answer.status}`);
|
|
2337
|
-
}
|
|
2338
|
-
return answer.json();
|
|
2339
|
-
}
|
|
2340
|
-
function asMinor(quoted, venue, currency) {
|
|
2341
|
-
const major2 = Number(quoted);
|
|
2342
|
-
if (!Number.isFinite(major2) || major2 <= 0) {
|
|
2343
|
-
throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
|
|
2344
|
-
}
|
|
2345
|
-
return Math.round(major2 * minorScaleOf(currency));
|
|
2346
|
-
}
|
|
2347
|
-
function spreadBpsOf(sorted) {
|
|
2348
|
-
const lowest = sorted[0] ?? 0;
|
|
2349
|
-
const highest = sorted[sorted.length - 1] ?? 0;
|
|
2350
|
-
return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
|
|
2351
|
-
}
|
|
2352
|
-
function middleOf(sorted) {
|
|
2353
|
-
const middle = Math.floor(sorted.length / 2);
|
|
2354
|
-
if (sorted.length % 2 === 1) {
|
|
2355
|
-
return sorted[middle] ?? 0;
|
|
2356
|
-
}
|
|
2357
|
-
return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
|
|
2358
|
-
}
|
|
2359
|
-
function refusals2(answers) {
|
|
2360
|
-
return answers.filter((answer) => answer.status === "rejected").map(
|
|
2361
|
-
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
2362
|
-
).join("; ");
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
// src/qr.ts
|
|
2366
|
-
var import_uqr = require("uqr");
|
|
2367
|
-
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
2368
|
-
function encodeForQr(destination) {
|
|
2369
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
2370
|
-
return `LIGHTNING:${scanned}`;
|
|
2371
|
-
}
|
|
2372
|
-
function invoiceToSvg(destination, options) {
|
|
2373
|
-
return svgOf(encodeForQr(destination), options);
|
|
2374
|
-
}
|
|
2375
|
-
function invoiceToDataUrl(destination, options) {
|
|
2376
|
-
return asDataUrl(invoiceToSvg(destination, options));
|
|
2377
|
-
}
|
|
2378
|
-
function lnurlToSvg(endpoint, options) {
|
|
2379
|
-
return svgOf(toLnurl(endpoint), options);
|
|
2380
|
-
}
|
|
2381
|
-
function lnurlToDataUrl(endpoint, options) {
|
|
2382
|
-
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
2383
|
-
}
|
|
2384
|
-
function spdToSvg(spd, options) {
|
|
2385
|
-
return svgOf(spd, options);
|
|
2386
|
-
}
|
|
2387
|
-
function spdToDataUrl(spd, options) {
|
|
2388
|
-
return asDataUrl(spdToSvg(spd, options));
|
|
2389
|
-
}
|
|
2390
|
-
function qrToSvg(payload, options) {
|
|
2391
|
-
return svgOf(payload, options);
|
|
2392
|
-
}
|
|
2393
|
-
function qrToDataUrl(payload, options) {
|
|
2394
|
-
return asDataUrl(qrToSvg(payload, options));
|
|
2395
|
-
}
|
|
2396
|
-
function svgOf(scanned, options) {
|
|
2397
|
-
const size = options?.size ?? 256;
|
|
2398
|
-
const color = options?.color ?? "#000";
|
|
2399
|
-
if (!SAFE_COLOR.test(color)) {
|
|
2400
|
-
throw new Error(`Invalid color: ${color}`);
|
|
2401
|
-
}
|
|
2402
|
-
return renderSvg(scanned, size, color);
|
|
2403
|
-
}
|
|
2404
|
-
function asDataUrl(svg) {
|
|
2405
|
-
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
2406
|
-
}
|
|
2407
|
-
function isLnAddress(destination) {
|
|
2408
|
-
const at = destination.indexOf("@");
|
|
2409
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
2410
|
-
}
|
|
2411
|
-
function renderSvg(content, size, color) {
|
|
2412
|
-
const { data } = (0, import_uqr.encode)(content);
|
|
2413
|
-
const modules = data.length;
|
|
2414
|
-
const cellSize = size / modules;
|
|
2415
|
-
let path = "";
|
|
2416
|
-
for (let y = 0; y < modules; y++) {
|
|
2417
|
-
for (let x = 0; x < modules; x++) {
|
|
2418
|
-
if (data[y][x]) {
|
|
2419
|
-
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
return [
|
|
2424
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
2425
|
-
`<rect width="100%" height="100%" fill="white"/>`,
|
|
2426
|
-
`<path d="${path}" fill="${color}"/>`,
|
|
2427
|
-
`</svg>`
|
|
2428
|
-
].join("");
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
// src/rail.ts
|
|
2432
|
-
var BANK = "bank";
|
|
2433
|
-
var LIGHTNING = "lightning";
|
|
2434
|
-
function bankRail(config) {
|
|
2435
|
-
return async (order) => {
|
|
2436
|
-
const expiresAt = config.expiresAt(order);
|
|
2437
|
-
const transfer = await bankTransfer({
|
|
2438
|
-
gateway: config.gateway,
|
|
2439
|
-
secret: config.secret,
|
|
2440
|
-
iban: config.iban,
|
|
2441
|
-
verifyUrl: config.verifyUrl,
|
|
2442
|
-
reference: order.reference,
|
|
2443
|
-
amountMinor: order.amountMinor,
|
|
2444
|
-
currency: order.currency,
|
|
2445
|
-
expiresAt,
|
|
2446
|
-
trigger: config.trigger,
|
|
2447
|
-
replay: config.replay,
|
|
2448
|
-
sealed: await config.sealed?.(order),
|
|
2449
|
-
variableSymbol: config.variableSymbol?.(order),
|
|
2450
|
-
webhookUrl: config.webhookUrl,
|
|
2451
|
-
allowPublicGateway: config.allowPublicGateway
|
|
2452
|
-
});
|
|
2453
|
-
return {
|
|
2454
|
-
id: transfer.id,
|
|
2455
|
-
rail: config.name ?? BANK,
|
|
2456
|
-
scan: transfer.spd,
|
|
2457
|
-
qr: transfer.spd,
|
|
2458
|
-
expiresAt
|
|
2459
|
-
};
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
function lightningRail(config) {
|
|
2463
|
-
return async (order) => {
|
|
2464
|
-
const payment = await config.gateway.createPayment(
|
|
2465
|
-
{
|
|
2466
|
-
lnAddresses: config.lnAddresses,
|
|
2467
|
-
amountMsat: await config.amountMsat(order),
|
|
2468
|
-
webhookUrl: config.webhookUrl
|
|
2469
|
-
},
|
|
2470
|
-
{
|
|
2471
|
-
idempotencyKey: config.idempotencyKey?.(order),
|
|
2472
|
-
trigger: config.trigger,
|
|
2473
|
-
replay: config.replay
|
|
2474
|
-
}
|
|
2475
|
-
);
|
|
2476
|
-
return {
|
|
2477
|
-
id: payment.id,
|
|
2478
|
-
rail: config.name ?? LIGHTNING,
|
|
2479
|
-
scan: payment.bolt11,
|
|
2480
|
-
qr: encodeForQr(payment.bolt11),
|
|
2481
|
-
expiresAt: payment.expiresAt
|
|
2482
|
-
};
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
2485
2999
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2486
3000
|
0 && (module.exports = {
|
|
3001
|
+
AmountError,
|
|
2487
3002
|
GatewayCheatError,
|
|
2488
3003
|
Gateways,
|
|
2489
|
-
IDEMPOTENCY_KEY_REUSED,
|
|
2490
3004
|
IdempotencyConflictError,
|
|
2491
|
-
NO_WALLET_AVAILABLE,
|
|
2492
3005
|
NoWalletAvailableError,
|
|
2493
|
-
PAYMENT_ALREADY_WATCHED,
|
|
2494
3006
|
ProblemError,
|
|
2495
|
-
REQUEST_IN_FLIGHT,
|
|
2496
3007
|
ThunderBridge,
|
|
2497
3008
|
UnverifiedRecipientError,
|
|
2498
3009
|
WrapRefusedError,
|
|
2499
3010
|
answerVerifyChallenge,
|
|
2500
|
-
|
|
2501
|
-
answerWebhookChallenge,
|
|
2502
|
-
answerWebhookChallengeRequest,
|
|
2503
|
-
bankRail,
|
|
2504
|
-
bankTransfer,
|
|
2505
|
-
bankVerifyEndpoint,
|
|
2506
|
-
bitstamp,
|
|
2507
|
-
coinbase,
|
|
2508
|
-
coinmate,
|
|
3011
|
+
carriesProof,
|
|
2509
3012
|
decodeInvoice,
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
isProblemType,
|
|
2514
|
-
isProvablyPaid,
|
|
2515
|
-
isProvablySettled,
|
|
2516
|
-
kraken,
|
|
2517
|
-
lightningRail,
|
|
2518
|
-
lnurlToDataUrl,
|
|
2519
|
-
lnurlToSvg,
|
|
2520
|
-
medianOf,
|
|
2521
|
-
minorScaleOf,
|
|
2522
|
-
minorUnitsOf,
|
|
2523
|
-
msatFor,
|
|
2524
|
-
parseSettlement,
|
|
2525
|
-
parseSettlementRequest,
|
|
2526
|
-
parseWatchedWebhook,
|
|
2527
|
-
parseWatchedWebhookRequest,
|
|
2528
|
-
parseWebhook,
|
|
2529
|
-
parseWebhookRequest,
|
|
3013
|
+
fiat,
|
|
3014
|
+
invoiceFrom,
|
|
3015
|
+
msat,
|
|
2530
3016
|
preimageMatchesHash,
|
|
2531
3017
|
proveOrigin,
|
|
2532
3018
|
proveSettlement,
|
|
2533
3019
|
proveWrapped,
|
|
2534
|
-
|
|
2535
|
-
|
|
3020
|
+
relayedVerifyUrl,
|
|
3021
|
+
sats,
|
|
2536
3022
|
seal,
|
|
2537
|
-
spdToDataUrl,
|
|
2538
|
-
spdToSvg,
|
|
2539
|
-
toLnurl,
|
|
2540
3023
|
unseal,
|
|
2541
|
-
verifyWebhookSignature,
|
|
2542
3024
|
wrapFeeCeiling
|
|
2543
3025
|
});
|