thunder-bridge 1.4.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -133
- package/dist/bank-CEIuNhV_.d.cts +896 -0
- package/dist/bank-M8flcMks.d.ts +896 -0
- package/dist/bank.cjs +338 -0
- package/dist/bank.d.cts +46 -0
- package/dist/bank.d.ts +46 -0
- package/dist/bank.js +310 -0
- package/dist/errors-DriqW-lp.d.cts +126 -0
- package/dist/errors-ZEUT-MhO.d.ts +126 -0
- package/dist/index.cjs +2108 -1235
- package/dist/index.d.cts +13 -505
- package/dist/index.d.ts +13 -505
- package/dist/index.js +2088 -1193
- package/dist/{server.cjs → nwc.cjs} +376 -720
- package/dist/nwc.d.cts +122 -0
- package/dist/nwc.d.ts +122 -0
- package/dist/{server.js → nwc.js} +368 -695
- 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 +259 -0
- package/dist/qr.d.cts +1 -0
- package/dist/qr.d.ts +1 -0
- package/dist/qr.js +224 -0
- package/dist/types-DYZ9EkmJ.d.cts +261 -0
- package/dist/types-DYZ9EkmJ.d.ts +261 -0
- package/openapi.yaml +29 -39
- 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
|
@@ -13,8 +13,8 @@ function hexToBytes(hex) {
|
|
|
13
13
|
}
|
|
14
14
|
return bytes;
|
|
15
15
|
}
|
|
16
|
-
function isHex(
|
|
17
|
-
return /^[0-9a-f]+$/i.test(
|
|
16
|
+
function isHex(text) {
|
|
17
|
+
return /^[0-9a-f]+$/i.test(text) && text.length % 2 === 0;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// ../core/sha256.ts
|
|
@@ -108,9 +108,6 @@ function sha256(bytes) {
|
|
|
108
108
|
}
|
|
109
109
|
return asBytes(state);
|
|
110
110
|
}
|
|
111
|
-
function sha256Hex(text2) {
|
|
112
|
-
return bytesToHex(sha256(new TextEncoder().encode(text2)));
|
|
113
|
-
}
|
|
114
111
|
function withPadding(bytes) {
|
|
115
112
|
const padded2 = new Uint8Array(
|
|
116
113
|
bytes.length + 1 + LENGTH_BYTES + BLOCK_BYTES - 1 & ~(BLOCK_BYTES - 1)
|
|
@@ -175,6 +172,7 @@ function rotate(value, by) {
|
|
|
175
172
|
|
|
176
173
|
// ../core/bolt11.ts
|
|
177
174
|
var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
175
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
178
176
|
var CHECKSUM_WORDS = 6;
|
|
179
177
|
var TIMESTAMP_WORDS = 7;
|
|
180
178
|
var SIGNATURE_WORDS = 104;
|
|
@@ -192,18 +190,27 @@ var HRP_MULTIPLIER_MSAT = {
|
|
|
192
190
|
n: MSAT_PER_BTC / 1e9,
|
|
193
191
|
p: MSAT_PER_BTC / 1e12
|
|
194
192
|
};
|
|
195
|
-
var
|
|
193
|
+
var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
|
|
196
194
|
function decodeInvoice(bolt11) {
|
|
197
195
|
const parts = splitBech32(bolt11);
|
|
198
196
|
if (parts === null) {
|
|
199
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
paymentHash: null,
|
|
199
|
+
descriptionHash: null,
|
|
200
|
+
amountMsat: null,
|
|
201
|
+
issuedAt: null,
|
|
202
|
+
expiresAt: null
|
|
203
|
+
};
|
|
200
204
|
}
|
|
201
205
|
const tagged = taggedFields(parts.words);
|
|
206
|
+
const issuedAt = issuedAtOf(parts.words);
|
|
207
|
+
const expiry = tagged.get(EXPIRY_TAG);
|
|
202
208
|
return {
|
|
203
209
|
paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
|
|
204
210
|
descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
|
|
205
211
|
amountMsat: amountFromHrp(parts.hrp),
|
|
206
|
-
|
|
212
|
+
issuedAt,
|
|
213
|
+
expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
|
|
207
214
|
};
|
|
208
215
|
}
|
|
209
216
|
function preimageMatchesHash(preimage, paymentHash) {
|
|
@@ -214,7 +221,7 @@ function preimageMatchesHash(preimage, paymentHash) {
|
|
|
214
221
|
}
|
|
215
222
|
function splitBech32(bolt11) {
|
|
216
223
|
const lower = bolt11.toLowerCase();
|
|
217
|
-
if (isBolt12Encoding(lower)) {
|
|
224
|
+
if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
|
|
218
225
|
return null;
|
|
219
226
|
}
|
|
220
227
|
const separator = lower.lastIndexOf("1");
|
|
@@ -233,7 +240,28 @@ function splitBech32(bolt11) {
|
|
|
233
240
|
}
|
|
234
241
|
words.push(value);
|
|
235
242
|
}
|
|
236
|
-
|
|
243
|
+
const hrp = lower.slice(0, separator);
|
|
244
|
+
if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
|
|
248
|
+
}
|
|
249
|
+
function expandedHrp(hrp) {
|
|
250
|
+
const codes = [...hrp].map((char) => char.charCodeAt(0));
|
|
251
|
+
return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
|
|
252
|
+
}
|
|
253
|
+
function bech32Polymod(values) {
|
|
254
|
+
let checksum = 1;
|
|
255
|
+
for (const value of values) {
|
|
256
|
+
const top = checksum >> 25;
|
|
257
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
258
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
259
|
+
if (top >> bit & 1) {
|
|
260
|
+
checksum ^= BECH32_GENERATOR[bit];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return checksum;
|
|
237
265
|
}
|
|
238
266
|
function isBolt12Encoding(lower) {
|
|
239
267
|
return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
|
|
@@ -263,19 +291,17 @@ function hexTag(fields, tag, expectedWords) {
|
|
|
263
291
|
}
|
|
264
292
|
return bytesToHex(wordsToBytes(words));
|
|
265
293
|
}
|
|
266
|
-
function
|
|
294
|
+
function issuedAtOf(words) {
|
|
267
295
|
if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
|
|
268
296
|
return null;
|
|
269
297
|
}
|
|
270
|
-
|
|
271
|
-
return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
|
|
298
|
+
return readNumber(words.slice(0, TIMESTAMP_WORDS));
|
|
272
299
|
}
|
|
273
300
|
function amountFromHrp(hrp) {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
301
|
+
const [, digits, multiplier] = HRP.exec(hrp) ?? [];
|
|
302
|
+
if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
|
|
276
303
|
return null;
|
|
277
304
|
}
|
|
278
|
-
const [, digits, multiplier] = match;
|
|
279
305
|
return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
|
|
280
306
|
}
|
|
281
307
|
function readNumber(words) {
|
|
@@ -304,36 +330,40 @@ var WalletRefused = class extends Error {
|
|
|
304
330
|
this.reason = reason;
|
|
305
331
|
}
|
|
306
332
|
};
|
|
307
|
-
var NoWalletAvailable = class extends Error {
|
|
308
|
-
wallets;
|
|
309
|
-
constructor(wallets) {
|
|
310
|
-
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
311
|
-
this.wallets = wallets;
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
333
|
|
|
315
334
|
// ../core/sealed.ts
|
|
316
|
-
var VERSION = "
|
|
335
|
+
var VERSION = "v2";
|
|
336
|
+
var UNPADDED_VERSION = "v1";
|
|
317
337
|
var IV_BYTES = 12;
|
|
318
338
|
var MIN_SECRET_CHARS = 32;
|
|
319
339
|
var MAX_PLAIN_BYTES = 3e3;
|
|
340
|
+
var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
|
|
341
|
+
var END_OF_TEXT = 128;
|
|
320
342
|
var INFO = new TextEncoder().encode("thunder-bridge/sealed");
|
|
321
|
-
async function seal(secret, plaintext) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
343
|
+
async function seal(secret, plaintext, paymentHash) {
|
|
344
|
+
return await sealUnder(
|
|
345
|
+
secret,
|
|
346
|
+
plaintext,
|
|
347
|
+
crypto.getRandomValues(new Uint8Array(IV_BYTES)),
|
|
348
|
+
boundTo(paymentHash)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
async function sealUnder(secret, plaintext, iv, additionalData) {
|
|
352
|
+
const body = padToClass(plaintext);
|
|
326
353
|
const key = await keyFor(secret);
|
|
327
|
-
const
|
|
328
|
-
|
|
354
|
+
const cipher = new Uint8Array(
|
|
355
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
|
|
356
|
+
);
|
|
329
357
|
const joined2 = new Uint8Array(iv.length + cipher.length);
|
|
330
358
|
joined2.set(iv);
|
|
331
359
|
joined2.set(cipher, iv.length);
|
|
332
360
|
return `${VERSION}.${toBase64Url(joined2)}`;
|
|
333
361
|
}
|
|
334
|
-
async function unseal(secret, sealed) {
|
|
362
|
+
async function unseal(secret, sealed, paymentHash) {
|
|
335
363
|
const key = await keyFor(secret);
|
|
336
|
-
|
|
364
|
+
const padded2 = sealed.startsWith(`${VERSION}.`);
|
|
365
|
+
const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
|
|
366
|
+
if (!padded2 && (!unbound || paymentHash !== void 0)) {
|
|
337
367
|
return null;
|
|
338
368
|
}
|
|
339
369
|
const joined2 = fromBase64Url(sealed.slice(VERSION.length + 1));
|
|
@@ -341,20 +371,50 @@ async function unseal(secret, sealed) {
|
|
|
341
371
|
return null;
|
|
342
372
|
}
|
|
343
373
|
try {
|
|
344
|
-
const body =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
const body = new Uint8Array(
|
|
375
|
+
await crypto.subtle.decrypt(
|
|
376
|
+
{
|
|
377
|
+
name: "AES-GCM",
|
|
378
|
+
iv: joined2.slice(0, IV_BYTES),
|
|
379
|
+
additionalData: padded2 ? boundTo(paymentHash) : void 0
|
|
380
|
+
},
|
|
381
|
+
key,
|
|
382
|
+
joined2.slice(IV_BYTES)
|
|
383
|
+
)
|
|
348
384
|
);
|
|
349
|
-
|
|
385
|
+
const text = padded2 ? unpad(body) : body;
|
|
386
|
+
return text === null ? null : new TextDecoder().decode(text);
|
|
350
387
|
} catch {
|
|
351
388
|
return null;
|
|
352
389
|
}
|
|
353
390
|
}
|
|
354
|
-
|
|
391
|
+
function refuseAWeakSecret(secret) {
|
|
355
392
|
if (secret.length < MIN_SECRET_CHARS) {
|
|
356
393
|
throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
|
|
357
394
|
}
|
|
395
|
+
}
|
|
396
|
+
function boundTo(paymentHash) {
|
|
397
|
+
return new TextEncoder().encode(
|
|
398
|
+
paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
function padToClass(plaintext) {
|
|
402
|
+
const text = new TextEncoder().encode(plaintext);
|
|
403
|
+
const size = SIZE_CLASSES.find((limit) => text.length <= limit);
|
|
404
|
+
if (size === void 0) {
|
|
405
|
+
throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text.length}`);
|
|
406
|
+
}
|
|
407
|
+
const body = new Uint8Array(size + 1);
|
|
408
|
+
body.set(text);
|
|
409
|
+
body[text.length] = END_OF_TEXT;
|
|
410
|
+
return body;
|
|
411
|
+
}
|
|
412
|
+
function unpad(body) {
|
|
413
|
+
const end = body.lastIndexOf(END_OF_TEXT);
|
|
414
|
+
return end === -1 ? null : body.subarray(0, end);
|
|
415
|
+
}
|
|
416
|
+
async function keyFor(secret) {
|
|
417
|
+
refuseAWeakSecret(secret);
|
|
358
418
|
const material = await crypto.subtle.importKey(
|
|
359
419
|
"raw",
|
|
360
420
|
new TextEncoder().encode(secret),
|
|
@@ -377,12 +437,12 @@ function toBase64Url(bytes) {
|
|
|
377
437
|
}
|
|
378
438
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
379
439
|
}
|
|
380
|
-
function fromBase64Url(
|
|
381
|
-
if (
|
|
440
|
+
function fromBase64Url(text) {
|
|
441
|
+
if (text.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text)) {
|
|
382
442
|
return null;
|
|
383
443
|
}
|
|
384
444
|
try {
|
|
385
|
-
const binary = atob(
|
|
445
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
386
446
|
const bytes = new Uint8Array(binary.length);
|
|
387
447
|
for (let at = 0; at < bytes.length; at++) {
|
|
388
448
|
bytes[at] = binary.charCodeAt(at);
|
|
@@ -539,8 +599,8 @@ function toBase64(bytes) {
|
|
|
539
599
|
}
|
|
540
600
|
return btoa(binary);
|
|
541
601
|
}
|
|
542
|
-
function fromBase64(
|
|
543
|
-
const binary = atob(
|
|
602
|
+
function fromBase64(text) {
|
|
603
|
+
const binary = atob(text);
|
|
544
604
|
const bytes = new Uint8Array(binary.length);
|
|
545
605
|
for (let at = 0; at < bytes.length; at++) {
|
|
546
606
|
bytes[at] = binary.charCodeAt(at);
|
|
@@ -548,14 +608,257 @@ function fromBase64(text2) {
|
|
|
548
608
|
return bytes;
|
|
549
609
|
}
|
|
550
610
|
|
|
611
|
+
// src/qr.ts
|
|
612
|
+
import { encode } from "uqr";
|
|
613
|
+
function toLightningUri(destination) {
|
|
614
|
+
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
615
|
+
return `LIGHTNING:${scanned}`;
|
|
616
|
+
}
|
|
617
|
+
function isLnAddress(destination) {
|
|
618
|
+
const at = destination.indexOf("@");
|
|
619
|
+
return at > 0 && destination.slice(at + 1).includes(".");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/errors.ts
|
|
623
|
+
var AmountError = class extends Error {
|
|
624
|
+
/**
|
|
625
|
+
* Whether a failure is one of these, without asking whether it is this exact
|
|
626
|
+
* class. Every entry point carries its own copy of the class, so a price
|
|
627
|
+
* refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
|
|
628
|
+
* imported from `thunder-bridge`. The name and the code are the same in every
|
|
629
|
+
* copy, so this holds where `instanceof` does not
|
|
630
|
+
*/
|
|
631
|
+
static is(failure) {
|
|
632
|
+
return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
|
|
633
|
+
}
|
|
634
|
+
code;
|
|
635
|
+
constructor(code, detail) {
|
|
636
|
+
super(`${code}: ${detail}`);
|
|
637
|
+
this.name = "AmountError";
|
|
638
|
+
this.code = code;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/currency.ts
|
|
643
|
+
var ISO_4217_MINOR_UNITS = {
|
|
644
|
+
BHD: 3,
|
|
645
|
+
BIF: 0,
|
|
646
|
+
CLP: 0,
|
|
647
|
+
CZK: 2,
|
|
648
|
+
DJF: 0,
|
|
649
|
+
EUR: 2,
|
|
650
|
+
GBP: 2,
|
|
651
|
+
ISK: 0,
|
|
652
|
+
IQD: 3,
|
|
653
|
+
JOD: 3,
|
|
654
|
+
JPY: 0,
|
|
655
|
+
KRW: 0,
|
|
656
|
+
KWD: 3,
|
|
657
|
+
LYD: 3,
|
|
658
|
+
OMR: 3,
|
|
659
|
+
PLN: 2,
|
|
660
|
+
PYG: 0,
|
|
661
|
+
RWF: 0,
|
|
662
|
+
TND: 3,
|
|
663
|
+
UGX: 0,
|
|
664
|
+
USD: 2,
|
|
665
|
+
VND: 0,
|
|
666
|
+
VUV: 0,
|
|
667
|
+
XAF: 0,
|
|
668
|
+
XOF: 0,
|
|
669
|
+
XPF: 0
|
|
670
|
+
};
|
|
671
|
+
function minorUnitsOf(currency) {
|
|
672
|
+
const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
|
|
673
|
+
if (digits === void 0) {
|
|
674
|
+
throw new AmountError(
|
|
675
|
+
"unknown-currency",
|
|
676
|
+
`${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return digits;
|
|
680
|
+
}
|
|
681
|
+
function minorScaleOf(currency) {
|
|
682
|
+
return 10 ** minorUnitsOf(currency);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/price.ts
|
|
686
|
+
var MSAT_PER_BTC2 = 1e11;
|
|
687
|
+
var BASIS_POINTS = 1e4;
|
|
688
|
+
var DEFAULT_HOLD_SECS = 60;
|
|
689
|
+
var DEFAULT_MIN_VENUES = 2;
|
|
690
|
+
var DEFAULT_MAX_SPREAD_BPS = 200;
|
|
691
|
+
var MILLIS = 1e3;
|
|
692
|
+
function coinbase(baseUrl = "https://api.coinbase.com") {
|
|
693
|
+
return async (currency) => {
|
|
694
|
+
const asked = currency.toUpperCase();
|
|
695
|
+
const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
|
|
696
|
+
const quoted = answer.data;
|
|
697
|
+
if (quoted?.currency !== asked) {
|
|
698
|
+
throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
|
|
699
|
+
}
|
|
700
|
+
return asMinor(quoted.amount, "coinbase", asked);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function kraken(baseUrl = "https://api.kraken.com") {
|
|
704
|
+
return async (currency) => {
|
|
705
|
+
const asked = currency.toUpperCase();
|
|
706
|
+
const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
|
|
707
|
+
if (answer.error?.length) {
|
|
708
|
+
throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
|
|
709
|
+
}
|
|
710
|
+
const pairs = Object.values(answer.result ?? {});
|
|
711
|
+
if (pairs.length !== 1) {
|
|
712
|
+
throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
|
|
713
|
+
}
|
|
714
|
+
return asMinor(pairs[0]?.c?.[0], "kraken", asked);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function bitstamp(baseUrl = "https://www.bitstamp.net") {
|
|
718
|
+
return async (currency) => {
|
|
719
|
+
const pair = `btc${currency.toLowerCase()}`;
|
|
720
|
+
const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
|
|
721
|
+
if (Array.isArray(answer)) {
|
|
722
|
+
throw new Error(`bitstamp has no ${pair} pair`);
|
|
723
|
+
}
|
|
724
|
+
return asMinor(answer.last, "bitstamp", currency);
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function coinmate(baseUrl = "https://coinmate.io") {
|
|
728
|
+
return async (currency) => {
|
|
729
|
+
const pair = `BTC_${currency.toUpperCase()}`;
|
|
730
|
+
const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
|
|
731
|
+
if (answer.error !== false) {
|
|
732
|
+
throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
|
|
733
|
+
}
|
|
734
|
+
return asMinor(answer.data?.last, "coinmate", currency);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
|
|
738
|
+
const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
|
|
739
|
+
const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
|
|
740
|
+
const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
|
|
741
|
+
const held = /* @__PURE__ */ new Map();
|
|
742
|
+
return async (currency) => {
|
|
743
|
+
const asked = currency.toUpperCase();
|
|
744
|
+
const remembered = held.get(asked);
|
|
745
|
+
if (remembered && Date.now() - remembered.at < holdFor) {
|
|
746
|
+
return remembered.price;
|
|
747
|
+
}
|
|
748
|
+
const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
|
|
749
|
+
const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
|
|
750
|
+
if (quoted.length < minVenues) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const spread = spreadBpsOf(quoted);
|
|
756
|
+
if (spread > maxSpread) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`venues disagree on ${asked} by ${Math.round(spread)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const price = middleOf(quoted);
|
|
762
|
+
held.set(asked, { at: Date.now(), price });
|
|
763
|
+
return price;
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
|
|
767
|
+
if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
|
|
768
|
+
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
769
|
+
}
|
|
770
|
+
if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
|
|
771
|
+
throw new Error(`${priceMinorPerBtc} is not a usable price`);
|
|
772
|
+
}
|
|
773
|
+
const spreadBps = options.spreadBps ?? 0;
|
|
774
|
+
if (!Number.isInteger(spreadBps) || spreadBps < 0) {
|
|
775
|
+
throw new Error("spreadBps must be a whole number of basis points, none or more");
|
|
776
|
+
}
|
|
777
|
+
const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
|
|
778
|
+
const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
|
|
779
|
+
const msat2 = (wanted + per - 1n) / per;
|
|
780
|
+
if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
781
|
+
throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
|
|
782
|
+
}
|
|
783
|
+
return Number(msat2);
|
|
784
|
+
}
|
|
785
|
+
async function asJson(url, venue) {
|
|
786
|
+
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
787
|
+
if (!answer.ok) {
|
|
788
|
+
throw new Error(`${venue} answered ${answer.status}`);
|
|
789
|
+
}
|
|
790
|
+
return answer.json();
|
|
791
|
+
}
|
|
792
|
+
function asMinor(quoted, venue, currency) {
|
|
793
|
+
const major = Number(quoted);
|
|
794
|
+
if (!Number.isFinite(major) || major <= 0) {
|
|
795
|
+
throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
|
|
796
|
+
}
|
|
797
|
+
return Math.round(major * minorScaleOf(currency));
|
|
798
|
+
}
|
|
799
|
+
function spreadBpsOf(sorted) {
|
|
800
|
+
const lowest = sorted[0] ?? 0;
|
|
801
|
+
const highest = sorted[sorted.length - 1] ?? 0;
|
|
802
|
+
return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
|
|
803
|
+
}
|
|
804
|
+
function middleOf(sorted) {
|
|
805
|
+
const middle = Math.floor(sorted.length / 2);
|
|
806
|
+
if (sorted.length % 2 === 1) {
|
|
807
|
+
return sorted[middle] ?? 0;
|
|
808
|
+
}
|
|
809
|
+
return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
|
|
810
|
+
}
|
|
811
|
+
function refusals(answers) {
|
|
812
|
+
return answers.filter((answer) => answer.status === "rejected").map(
|
|
813
|
+
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
814
|
+
).join("; ");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/amount.ts
|
|
818
|
+
var MSAT_PER_SAT = 1e3;
|
|
819
|
+
var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
|
|
820
|
+
function msat(exact) {
|
|
821
|
+
if (!Number.isSafeInteger(exact) || exact < 1) {
|
|
822
|
+
throw new AmountError(
|
|
823
|
+
"not-whole-millisatoshi",
|
|
824
|
+
`${exact} is not a payable count of millisatoshi`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
return exact;
|
|
828
|
+
}
|
|
829
|
+
async function amountNow(amount) {
|
|
830
|
+
return typeof amount === "function" ? msat(await amount()) : msat(amount);
|
|
831
|
+
}
|
|
832
|
+
|
|
551
833
|
// ../core/ed25519.ts
|
|
552
834
|
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
835
|
+
var SMALL_ORDER = [
|
|
836
|
+
"00".repeat(32),
|
|
837
|
+
`01${"00".repeat(31)}`,
|
|
838
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
839
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
840
|
+
`ec${"ff".repeat(30)}7f`,
|
|
841
|
+
`ed${"ff".repeat(30)}7f`,
|
|
842
|
+
`ee${"ff".repeat(30)}7f`
|
|
843
|
+
];
|
|
553
844
|
|
|
554
845
|
// src/webhook.ts
|
|
555
846
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
556
|
-
function
|
|
847
|
+
async function answerVerifyChallenge(request) {
|
|
848
|
+
if (request.method !== "POST") {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const nonce = nonceOf(await request.clone().text(), VERIFY_CHALLENGE);
|
|
852
|
+
return nonce === null ? null : answered(nonce);
|
|
853
|
+
}
|
|
854
|
+
function answered(nonce) {
|
|
855
|
+
return new Response(JSON.stringify({ nonce }), {
|
|
856
|
+
headers: { "content-type": "application/json" }
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function nonceOf(text, type) {
|
|
557
860
|
try {
|
|
558
|
-
const said = JSON.parse(
|
|
861
|
+
const said = JSON.parse(text);
|
|
559
862
|
if (said["type"] !== type || typeof said["nonce"] !== "string") {
|
|
560
863
|
return null;
|
|
561
864
|
}
|
|
@@ -564,16 +867,13 @@ function nonceOf(text2, type) {
|
|
|
564
867
|
return null;
|
|
565
868
|
}
|
|
566
869
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (request.method !== "POST") {
|
|
573
|
-
return null;
|
|
870
|
+
|
|
871
|
+
// src/rail.ts
|
|
872
|
+
async function msatForOrder(order, amount, rate) {
|
|
873
|
+
if (amount !== void 0) {
|
|
874
|
+
return await amountNow(amount(order));
|
|
574
875
|
}
|
|
575
|
-
|
|
576
|
-
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
876
|
+
return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
|
|
577
877
|
}
|
|
578
878
|
|
|
579
879
|
// src/nwc.ts
|
|
@@ -694,7 +994,7 @@ function nwcVerifyEndpoint(config) {
|
|
|
694
994
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
695
995
|
};
|
|
696
996
|
return async (request) => {
|
|
697
|
-
const consented = await
|
|
997
|
+
const consented = await answerVerifyChallenge(request);
|
|
698
998
|
if (consented !== null) {
|
|
699
999
|
return consented;
|
|
700
1000
|
}
|
|
@@ -854,505 +1154,14 @@ function isSecureRelay(relay) {
|
|
|
854
1154
|
return false;
|
|
855
1155
|
}
|
|
856
1156
|
}
|
|
857
|
-
|
|
858
|
-
// ../core/url.ts
|
|
859
|
-
var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
|
|
860
|
-
function publicHttps(raw) {
|
|
861
|
-
let url;
|
|
862
|
-
try {
|
|
863
|
-
url = new URL(raw);
|
|
864
|
-
} catch {
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
|
|
868
|
-
}
|
|
869
|
-
function sameOrigin(one, other) {
|
|
870
|
-
try {
|
|
871
|
-
return new URL(one).origin === new URL(other).origin;
|
|
872
|
-
} catch {
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
function publicAddress(literal) {
|
|
877
|
-
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
878
|
-
}
|
|
879
|
-
function hostIsPublic(host) {
|
|
880
|
-
if (host.startsWith("[")) {
|
|
881
|
-
return publicAddress(host.slice(1, -1));
|
|
882
|
-
}
|
|
883
|
-
const name = host.replace(/\.$/, "");
|
|
884
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
|
|
885
|
-
return publicAddress(name);
|
|
886
|
-
}
|
|
887
|
-
const labels = name.split(".");
|
|
888
|
-
if (labels.length < 2 || labels.some((label) => label === "")) {
|
|
889
|
-
return false;
|
|
890
|
-
}
|
|
891
|
-
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
892
|
-
}
|
|
893
|
-
function ipv4IsGlobal(literal) {
|
|
894
|
-
const parts = literal.split(".").map(Number);
|
|
895
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
896
|
-
return false;
|
|
897
|
-
}
|
|
898
|
-
const [a, b] = parts;
|
|
899
|
-
if (a === 0 || a === 10 || a === 127 || a >= 240) {
|
|
900
|
-
return false;
|
|
901
|
-
}
|
|
902
|
-
if (a === 100 && b >= 64 && b < 128) {
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
if (a === 169 && b === 254) {
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
if (a === 172 && b >= 16 && b < 32) {
|
|
909
|
-
return false;
|
|
910
|
-
}
|
|
911
|
-
if (a === 192 && b === 168) {
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
function ipv6IsGlobal(literal) {
|
|
917
|
-
const groups = ipv6Groups(literal.toLowerCase());
|
|
918
|
-
if (groups.length !== 8 || groups.some(Number.isNaN)) {
|
|
919
|
-
return false;
|
|
920
|
-
}
|
|
921
|
-
const leading = groups.slice(0, 5).every((group) => group === 0);
|
|
922
|
-
if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
if (leading && groups[5] === 65535) {
|
|
926
|
-
const [high, low] = [groups[6], groups[7]];
|
|
927
|
-
return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
928
|
-
}
|
|
929
|
-
const first = groups[0];
|
|
930
|
-
if ((first & 65024) === 64512) {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
if ((first & 65472) === 65152) {
|
|
934
|
-
return false;
|
|
935
|
-
}
|
|
936
|
-
return true;
|
|
937
|
-
}
|
|
938
|
-
function ipv6Groups(address) {
|
|
939
|
-
const [head = "", tail] = address.split("::");
|
|
940
|
-
const left = head.split(":").filter((group) => group !== "");
|
|
941
|
-
const right = (tail ?? "").split(":").filter((group) => group !== "");
|
|
942
|
-
const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
|
|
943
|
-
return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// ../core/outbound.ts
|
|
947
|
-
var HTTP_TIMEOUT_MS = 15e3;
|
|
948
|
-
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
949
|
-
var HOPS_FOLLOWED = 2;
|
|
950
|
-
var REDIRECTS = [301, 302, 303, 307, 308];
|
|
951
|
-
var KEEPS_THE_METHOD = [307, 308];
|
|
952
|
-
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
953
|
-
var BODY_LIMIT_BYTES = 262144;
|
|
954
|
-
var throughFetch = (url, sent, signal) => fetch(url, {
|
|
955
|
-
method: sent.method ?? "GET",
|
|
956
|
-
headers: sent.headers ?? {},
|
|
957
|
-
body: sent.body,
|
|
958
|
-
redirect: "manual",
|
|
959
|
-
signal
|
|
960
|
-
});
|
|
961
|
-
var send = throughFetch;
|
|
962
|
-
async function ask(url, sent = {}) {
|
|
963
|
-
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
|
|
964
|
-
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
965
|
-
let target = url;
|
|
966
|
-
let carried = sent;
|
|
967
|
-
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
968
|
-
const at = await addressesToReach(target);
|
|
969
|
-
const response = await send(target, carried, signal, at);
|
|
970
|
-
if (!REDIRECTS.includes(response.status)) {
|
|
971
|
-
return await answerOf(response);
|
|
972
|
-
}
|
|
973
|
-
const location = response.headers.get("location");
|
|
974
|
-
if (location === null) {
|
|
975
|
-
throw new Error(`${target} answered ${response.status} without saying where to`);
|
|
976
|
-
}
|
|
977
|
-
await response.body?.cancel();
|
|
978
|
-
const next = new URL(location, target).toString();
|
|
979
|
-
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
980
|
-
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
981
|
-
target = next;
|
|
982
|
-
}
|
|
983
|
-
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
984
|
-
}
|
|
985
|
-
async function addressesToReach(url) {
|
|
986
|
-
if (!publicHttps(url)) {
|
|
987
|
-
throw new Error(`${url} is not a public https URL`);
|
|
988
|
-
}
|
|
989
|
-
const found = await resolved(url);
|
|
990
|
-
if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
|
|
991
|
-
throw new Error(`${url} resolves to an address we do not reach`);
|
|
992
|
-
}
|
|
993
|
-
return found;
|
|
994
|
-
}
|
|
995
|
-
async function resolved(url) {
|
|
996
|
-
const { lookup } = await import("dns/promises");
|
|
997
|
-
const { setTimeout: sleep } = await import("timers/promises");
|
|
998
|
-
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
999
|
-
const found = await Promise.race([
|
|
1000
|
-
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
1001
|
-
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
1002
|
-
]);
|
|
1003
|
-
return found ?? [];
|
|
1004
|
-
}
|
|
1005
|
-
async function answerOf(response) {
|
|
1006
|
-
const { status, ok, headers } = response;
|
|
1007
|
-
const reader = response.body?.getReader();
|
|
1008
|
-
if (!reader) {
|
|
1009
|
-
return { status, ok, body: "", truncated: false, headers };
|
|
1010
|
-
}
|
|
1011
|
-
const read = [];
|
|
1012
|
-
let bytes = 0;
|
|
1013
|
-
while (bytes <= BODY_LIMIT_BYTES) {
|
|
1014
|
-
const { done, value } = await reader.read();
|
|
1015
|
-
if (done) {
|
|
1016
|
-
return { status, ok, body: text(read), truncated: false, headers };
|
|
1017
|
-
}
|
|
1018
|
-
read.push(value);
|
|
1019
|
-
bytes += value.length;
|
|
1020
|
-
}
|
|
1021
|
-
await reader.cancel();
|
|
1022
|
-
return { status, ok, body: text(read), truncated: true, headers };
|
|
1023
|
-
}
|
|
1024
|
-
function withoutBody(sent) {
|
|
1025
|
-
const headers = { ...sent.headers };
|
|
1026
|
-
delete headers["content-type"];
|
|
1027
|
-
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1028
|
-
}
|
|
1029
|
-
function withoutCredentials(sent) {
|
|
1030
|
-
const headers = { ...sent.headers };
|
|
1031
|
-
for (const named of CREDENTIALS) {
|
|
1032
|
-
delete headers[named];
|
|
1033
|
-
}
|
|
1034
|
-
return { ...sent, headers };
|
|
1035
|
-
}
|
|
1036
|
-
function text(chunks) {
|
|
1037
|
-
const decoder = new TextDecoder();
|
|
1038
|
-
let decoded = "";
|
|
1039
|
-
for (const chunk of chunks) {
|
|
1040
|
-
decoded += decoder.decode(chunk, { stream: true });
|
|
1041
|
-
}
|
|
1042
|
-
return decoded + decoder.decode();
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// ../core/lnurl.ts
|
|
1046
|
-
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1047
|
-
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1048
|
-
var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
1049
|
-
var MIN_PACE_SECS = 1;
|
|
1050
|
-
var MAX_PACE_SECS = 3600;
|
|
1051
|
-
var MIN_PER_SECOND = 0.01;
|
|
1052
|
-
var MAX_PER_SECOND = 100;
|
|
1053
|
-
async function resolve(addresses, amountMsat) {
|
|
1054
|
-
const served = await firstThatServes(
|
|
1055
|
-
addresses,
|
|
1056
|
-
(address, deadline) => resolveAddress(address, amountMsat, deadline)
|
|
1057
|
-
);
|
|
1058
|
-
return served.won;
|
|
1059
|
-
}
|
|
1060
|
-
async function firstThatServes(addresses, attempt) {
|
|
1061
|
-
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1062
|
-
const refusals = [];
|
|
1063
|
-
for (const address of addresses) {
|
|
1064
|
-
if (deadline.aborted) {
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
try {
|
|
1068
|
-
return { won: await attempt(address, deadline), refusals };
|
|
1069
|
-
} catch (refusal) {
|
|
1070
|
-
if (!(refusal instanceof WalletRefused)) {
|
|
1071
|
-
throw refusal;
|
|
1072
|
-
}
|
|
1073
|
-
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1074
|
-
refusals.push({ address, reason: refusal.reason });
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
throw new NoWalletAvailable(refusals);
|
|
1078
|
-
}
|
|
1079
|
-
async function probe(address, amountMsat, deadline) {
|
|
1080
|
-
const [user, domain] = splitAddress(address);
|
|
1081
|
-
if (cannotReleaseAPreimage(domain)) {
|
|
1082
|
-
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1083
|
-
}
|
|
1084
|
-
const pay = await answered(`https://${domain}/.well-known/lnurlp/${user}`, deadline);
|
|
1085
|
-
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1086
|
-
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1087
|
-
}
|
|
1088
|
-
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1089
|
-
throw new WalletRefused(
|
|
1090
|
-
"amount-not-accepted",
|
|
1091
|
-
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
return { address, pay };
|
|
1095
|
-
}
|
|
1096
|
-
async function resolveAddress(address, amountMsat, deadline) {
|
|
1097
|
-
const { pay } = await probe(address, amountMsat, deadline);
|
|
1098
|
-
const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
|
|
1099
|
-
if (!invoice.pr) {
|
|
1100
|
-
throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1101
|
-
}
|
|
1102
|
-
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1103
|
-
throw new WalletRefused(
|
|
1104
|
-
"cannot-prove-delivery",
|
|
1105
|
-
`${address} publishes no usable LUD-21 verify URL`
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
const issued = decodeIssued(address, invoice.pr);
|
|
1109
|
-
if (issued.amountMsat !== amountMsat) {
|
|
1110
|
-
throw new WalletRefused(
|
|
1111
|
-
"invoice-refused",
|
|
1112
|
-
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1116
|
-
throw new WalletRefused(
|
|
1117
|
-
"invoice-refused",
|
|
1118
|
-
`${address} does not bind its invoice to the metadata it serves`
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
address,
|
|
1123
|
-
bolt11: invoice.pr,
|
|
1124
|
-
verifyUrl: invoice.verify,
|
|
1125
|
-
paymentHash: issued.paymentHash,
|
|
1126
|
-
expiresAt: issued.expiresAt
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
async function answered(url, deadline) {
|
|
1130
|
-
try {
|
|
1131
|
-
return await fetchJson(url, deadline);
|
|
1132
|
-
} catch (cause) {
|
|
1133
|
-
throw new WalletRefused("unreachable", String(cause));
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
function decodeIssued(address, bolt11) {
|
|
1137
|
-
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1138
|
-
if (paymentHash === null || expiresAt === null) {
|
|
1139
|
-
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1140
|
-
}
|
|
1141
|
-
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1142
|
-
}
|
|
1143
|
-
async function checkSettled(verifyUrl, paymentHash) {
|
|
1144
|
-
const answer = await answeredJson(verifyUrl);
|
|
1145
|
-
const asked = {
|
|
1146
|
-
pace: paceAskedFor(answer.headers),
|
|
1147
|
-
ceiling: ceilingAskedFor(answer.headers)
|
|
1148
|
-
};
|
|
1149
|
-
if (!answer.said.settled || !answer.said.preimage) {
|
|
1150
|
-
return { preimage: null, ...asked };
|
|
1151
|
-
}
|
|
1152
|
-
if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
|
|
1153
|
-
throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
|
|
1154
|
-
}
|
|
1155
|
-
return { preimage: answer.said.preimage, ...asked };
|
|
1156
|
-
}
|
|
1157
|
-
function paceAskedFor(headers) {
|
|
1158
|
-
const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
|
|
1159
|
-
if (!asked) {
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
|
|
1163
|
-
}
|
|
1164
|
-
function ceilingAskedFor(headers) {
|
|
1165
|
-
const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
|
|
1166
|
-
if (!asked) {
|
|
1167
|
-
return null;
|
|
1168
|
-
}
|
|
1169
|
-
const perWindow = Number(asked[1]);
|
|
1170
|
-
const window = asked[2] === void 0 ? 1 : Number(asked[2]);
|
|
1171
|
-
if (perWindow === 0 || window === 0) {
|
|
1172
|
-
return null;
|
|
1173
|
-
}
|
|
1174
|
-
return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
|
|
1175
|
-
}
|
|
1176
|
-
function cannotReleaseAPreimage(host) {
|
|
1177
|
-
const lowered = host.toLowerCase();
|
|
1178
|
-
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1179
|
-
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
function withAmount(callback, amountMsat) {
|
|
1183
|
-
const separator = callback.includes("?") ? "&" : "?";
|
|
1184
|
-
return `${callback}${separator}amount=${amountMsat}`;
|
|
1185
|
-
}
|
|
1186
|
-
function splitAddress(address) {
|
|
1187
|
-
const [user, domain, ...rest] = address.split("@");
|
|
1188
|
-
if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
|
|
1189
|
-
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
1190
|
-
}
|
|
1191
|
-
return [user, domain];
|
|
1192
|
-
}
|
|
1193
|
-
function isNothingButAHost(domain) {
|
|
1194
|
-
try {
|
|
1195
|
-
const parsed = new URL(`https://${domain}/`);
|
|
1196
|
-
return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
|
|
1197
|
-
} catch {
|
|
1198
|
-
return false;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
async function fetchJson(url, deadline) {
|
|
1202
|
-
return (await answeredJson(url, deadline)).said;
|
|
1203
|
-
}
|
|
1204
|
-
async function answeredJson(url, deadline) {
|
|
1205
|
-
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
1206
|
-
if (!answer.ok) {
|
|
1207
|
-
throw new Error(`${url} answered ${answer.status}`);
|
|
1208
|
-
}
|
|
1209
|
-
if (answer.truncated) {
|
|
1210
|
-
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
1211
|
-
}
|
|
1212
|
-
return { said: JSON.parse(answer.body), headers: answer.headers };
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// ../core/hmac.ts
|
|
1216
|
-
async function hmacHex(secret, payload) {
|
|
1217
|
-
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
1218
|
-
const key = await crypto.subtle.importKey(
|
|
1219
|
-
"raw",
|
|
1220
|
-
asBuffer(raw),
|
|
1221
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
1222
|
-
false,
|
|
1223
|
-
["sign"]
|
|
1224
|
-
);
|
|
1225
|
-
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
1226
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
|
|
1227
|
-
}
|
|
1228
|
-
function equalInConstantTime(one, other) {
|
|
1229
|
-
if (one.length !== other.length) {
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
1232
|
-
let difference = 0;
|
|
1233
|
-
for (let at = 0; at < one.length; at++) {
|
|
1234
|
-
difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
1235
|
-
}
|
|
1236
|
-
return difference === 0;
|
|
1237
|
-
}
|
|
1238
|
-
function asBuffer(bytes) {
|
|
1239
|
-
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// src/errors.ts
|
|
1243
|
-
var ProblemError = class extends Error {
|
|
1244
|
-
type;
|
|
1245
|
-
title;
|
|
1246
|
-
status;
|
|
1247
|
-
detail;
|
|
1248
|
-
constructor(problem) {
|
|
1249
|
-
const title = problem.title ?? "Request failed";
|
|
1250
|
-
super(problem.detail ? `${title}: ${problem.detail}` : title);
|
|
1251
|
-
this.name = "ProblemError";
|
|
1252
|
-
this.type = problem.type ?? "about:blank";
|
|
1253
|
-
this.title = title;
|
|
1254
|
-
this.status = problem.status ?? 0;
|
|
1255
|
-
this.detail = problem.detail ?? null;
|
|
1256
|
-
}
|
|
1257
|
-
};
|
|
1258
|
-
function isProblemType(problem, type) {
|
|
1259
|
-
return problem.type === type;
|
|
1260
|
-
}
|
|
1261
|
-
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
1262
|
-
var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
|
|
1263
|
-
var NoWalletAvailableError = class extends ProblemError {
|
|
1264
|
-
wallets;
|
|
1265
|
-
constructor(problem, wallets) {
|
|
1266
|
-
super({ ...problem, type: NO_WALLET_AVAILABLE });
|
|
1267
|
-
this.name = "NoWalletAvailableError";
|
|
1268
|
-
this.wallets = wallets;
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
// src/qr.ts
|
|
1273
|
-
import { encode } from "uqr";
|
|
1274
|
-
function encodeForQr(destination) {
|
|
1275
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1276
|
-
return `LIGHTNING:${scanned}`;
|
|
1277
|
-
}
|
|
1278
|
-
function isLnAddress(destination) {
|
|
1279
|
-
const at = destination.indexOf("@");
|
|
1280
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// src/relay.ts
|
|
1284
|
-
var DEFAULT_POLL_EVERY_SECS2 = 5;
|
|
1285
|
-
var WALLET = "w";
|
|
1286
|
-
function lightningVerifyEndpoint(config) {
|
|
1287
|
-
const paced = {
|
|
1288
|
-
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS2}`
|
|
1289
|
-
};
|
|
1290
|
-
return async (request) => {
|
|
1291
|
-
const consented = await answerVerifyChallengeRequest(request);
|
|
1292
|
-
if (consented !== null) {
|
|
1293
|
-
return consented;
|
|
1294
|
-
}
|
|
1295
|
-
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
1296
|
-
if (sealed === null) {
|
|
1297
|
-
return Response.json({ settled: false }, { status: 400 });
|
|
1298
|
-
}
|
|
1299
|
-
const opened = await unseal(config.secret, sealed);
|
|
1300
|
-
if (opened === null) {
|
|
1301
|
-
return Response.json({ settled: false }, { status: 403 });
|
|
1302
|
-
}
|
|
1303
|
-
const wallet = JSON.parse(opened);
|
|
1304
|
-
const asked = await checkSettled(wallet.url, wallet.hash).catch(() => null);
|
|
1305
|
-
if (asked === null) {
|
|
1306
|
-
return Response.json({ settled: false }, { status: 502 });
|
|
1307
|
-
}
|
|
1308
|
-
return Response.json(
|
|
1309
|
-
{ settled: asked.preimage !== null, preimage: asked.preimage },
|
|
1310
|
-
{ headers: paced }
|
|
1311
|
-
);
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
1315
|
-
const relayed = new URL(endpoint);
|
|
1316
|
-
relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
|
|
1317
|
-
return relayed.toString();
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// src/rail.ts
|
|
1321
|
-
var LIGHTNING = "lightning";
|
|
1322
|
-
function blindLightningRail(config) {
|
|
1323
|
-
return async (order) => {
|
|
1324
|
-
const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
1325
|
-
const relay = config.relayVerifyThrough;
|
|
1326
|
-
const watched = await config.gateway.watchPayment({
|
|
1327
|
-
paymentHash: resolved2.paymentHash,
|
|
1328
|
-
verifyUrl: relay ? await relayedVerifyUrl(
|
|
1329
|
-
relay.endpoint,
|
|
1330
|
-
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
1331
|
-
relay.secret
|
|
1332
|
-
) : resolved2.verifyUrl,
|
|
1333
|
-
expiresAt: resolved2.expiresAt,
|
|
1334
|
-
trigger: config.trigger,
|
|
1335
|
-
replay: config.replay,
|
|
1336
|
-
sealed: await config.sealed?.(order),
|
|
1337
|
-
webhookUrl: config.webhookUrl
|
|
1338
|
-
});
|
|
1339
|
-
return {
|
|
1340
|
-
id: watched.id,
|
|
1341
|
-
rail: config.name ?? LIGHTNING,
|
|
1342
|
-
scan: resolved2.bolt11,
|
|
1343
|
-
qr: encodeForQr(resolved2.bolt11),
|
|
1344
|
-
expiresAt: resolved2.expiresAt
|
|
1345
|
-
};
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
function nwcRail(config) {
|
|
1157
|
+
function nwcRail(gateway, config) {
|
|
1349
1158
|
return async (order) => {
|
|
1350
1159
|
const invoice = await nwcInvoice(
|
|
1351
1160
|
config.connection,
|
|
1352
|
-
await config.
|
|
1161
|
+
await msatForOrder(order, config.amount, config.rate),
|
|
1353
1162
|
config.description?.(order) ?? order.reference
|
|
1354
1163
|
);
|
|
1355
|
-
const watched = await
|
|
1164
|
+
const watched = await gateway.watch({
|
|
1356
1165
|
paymentHash: invoice.paymentHash,
|
|
1357
1166
|
verifyUrl: await nwcVerifyUrl(
|
|
1358
1167
|
config.verifyThrough.endpoint,
|
|
@@ -1362,158 +1171,25 @@ function nwcRail(config) {
|
|
|
1362
1171
|
expiresAt: invoice.expiresAt,
|
|
1363
1172
|
trigger: config.trigger,
|
|
1364
1173
|
replay: config.replay,
|
|
1365
|
-
sealed:
|
|
1174
|
+
sealed: config.sealed ? await seal(
|
|
1175
|
+
config.sealed.secret,
|
|
1176
|
+
JSON.stringify(config.sealed.data(order)),
|
|
1177
|
+
invoice.paymentHash
|
|
1178
|
+
) : void 0,
|
|
1366
1179
|
webhookUrl: config.webhookUrl
|
|
1367
1180
|
});
|
|
1368
1181
|
return {
|
|
1369
1182
|
id: watched.id,
|
|
1370
|
-
|
|
1183
|
+
paymentHash: invoice.paymentHash,
|
|
1184
|
+
rail: config.name ?? "lightning",
|
|
1371
1185
|
scan: invoice.bolt11,
|
|
1372
|
-
qr:
|
|
1186
|
+
qr: toLightningUri(invoice.bolt11),
|
|
1373
1187
|
expiresAt: invoice.expiresAt
|
|
1374
1188
|
};
|
|
1375
1189
|
};
|
|
1376
1190
|
}
|
|
1377
|
-
async function invoiceFrom(lnAddresses, amountMsat) {
|
|
1378
|
-
try {
|
|
1379
|
-
return await resolve(lnAddresses, amountMsat);
|
|
1380
|
-
} catch (refused) {
|
|
1381
|
-
if (refused instanceof NoWalletAvailable) {
|
|
1382
|
-
throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
|
|
1383
|
-
}
|
|
1384
|
-
throw refused;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// src/trigger.ts
|
|
1389
|
-
var NONCE_BYTES2 = 16;
|
|
1390
|
-
function lnurlPayEndpoint(config) {
|
|
1391
|
-
return async (request) => {
|
|
1392
|
-
const url = new URL(request.url);
|
|
1393
|
-
const asked = url.searchParams.get("to");
|
|
1394
|
-
try {
|
|
1395
|
-
return asked === null ? await offer(config, url) : await mint(config, url, asked);
|
|
1396
|
-
} catch (failure) {
|
|
1397
|
-
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
function watchTicketEndpoint(config) {
|
|
1402
|
-
return async (request) => {
|
|
1403
|
-
const offered = await offeredSecret(request);
|
|
1404
|
-
if (!equalInConstantTime(sha256Hex(offered), sha256Hex(config.watchSecret))) {
|
|
1405
|
-
return Response.json({ reason: "not the watch secret" }, { status: 403 });
|
|
1406
|
-
}
|
|
1407
|
-
return await issue(config);
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
function publicWatchTicketEndpoint(config) {
|
|
1411
|
-
return () => issue(config);
|
|
1412
|
-
}
|
|
1413
|
-
async function offer(config, url) {
|
|
1414
|
-
const amountMsat = await config.amountMsat();
|
|
1415
|
-
const quote = await config.gateway.createQuote({
|
|
1416
|
-
lnAddresses: config.lnAddresses,
|
|
1417
|
-
amountMsat
|
|
1418
|
-
});
|
|
1419
|
-
const nonce = randomNonce();
|
|
1420
|
-
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
1421
|
-
callback.searchParams.set("to", quote.lnAddress);
|
|
1422
|
-
callback.searchParams.set("msat", String(amountMsat));
|
|
1423
|
-
callback.searchParams.set("n", nonce);
|
|
1424
|
-
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
|
|
1425
|
-
return Response.json({
|
|
1426
|
-
tag: "payRequest",
|
|
1427
|
-
callback: callback.toString(),
|
|
1428
|
-
metadata: quote.metadata,
|
|
1429
|
-
minSendable: amountMsat,
|
|
1430
|
-
maxSendable: amountMsat
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
async function mint(config, url, address) {
|
|
1434
|
-
const amountMsat = Number(url.searchParams.get("msat"));
|
|
1435
|
-
const nonce = url.searchParams.get("n") ?? "";
|
|
1436
|
-
const signature = url.searchParams.get("sig") ?? "";
|
|
1437
|
-
const expected = await sign(config.secret, address, amountMsat, nonce);
|
|
1438
|
-
if (!equalInConstantTime(signature, expected)) {
|
|
1439
|
-
return refuse("this callback was not signed here");
|
|
1440
|
-
}
|
|
1441
|
-
const wanted = url.searchParams.get("amount");
|
|
1442
|
-
if (wanted !== null && Number(wanted) !== amountMsat) {
|
|
1443
|
-
return refuse(`this trigger costs exactly ${amountMsat} msat`);
|
|
1444
|
-
}
|
|
1445
|
-
const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
|
|
1446
|
-
return Response.json({
|
|
1447
|
-
status: "OK",
|
|
1448
|
-
pr: minted.bolt11,
|
|
1449
|
-
routes: [],
|
|
1450
|
-
verify: minted.verifyUrl
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
1454
|
-
const payment = await config.gateway.createPayment(
|
|
1455
|
-
{ lnAddresses: [address], amountMsat },
|
|
1456
|
-
{ idempotencyKey: nonce, trigger: config.watchSecret, replay: config.replay }
|
|
1457
|
-
);
|
|
1458
|
-
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
1459
|
-
}
|
|
1460
|
-
async function mintBlind(config, address, amountMsat) {
|
|
1461
|
-
const resolved2 = await resolve([address], amountMsat);
|
|
1462
|
-
const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
|
|
1463
|
-
const locked = config.sealed;
|
|
1464
|
-
try {
|
|
1465
|
-
await config.gateway.watchPayment({
|
|
1466
|
-
paymentHash: resolved2.paymentHash,
|
|
1467
|
-
verifyUrl: resolved2.verifyUrl,
|
|
1468
|
-
expiresAt: resolved2.expiresAt,
|
|
1469
|
-
trigger: config.watchSecret,
|
|
1470
|
-
replay: config.replay,
|
|
1471
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
1472
|
-
});
|
|
1473
|
-
} catch (refused) {
|
|
1474
|
-
if (!alreadyWatched(refused)) {
|
|
1475
|
-
throw refused;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
|
|
1479
|
-
}
|
|
1480
|
-
function alreadyWatched(refused) {
|
|
1481
|
-
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
1482
|
-
}
|
|
1483
|
-
function sign(secret, address, amountMsat, nonce) {
|
|
1484
|
-
return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
|
|
1485
|
-
}
|
|
1486
|
-
function randomNonce() {
|
|
1487
|
-
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES2));
|
|
1488
|
-
let hex = "";
|
|
1489
|
-
for (const byte of bytes) {
|
|
1490
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
1491
|
-
}
|
|
1492
|
-
return hex;
|
|
1493
|
-
}
|
|
1494
|
-
function refuse(reason) {
|
|
1495
|
-
return Response.json({ status: "ERROR", reason });
|
|
1496
|
-
}
|
|
1497
|
-
async function issue(config) {
|
|
1498
|
-
const issued = await config.gateway.createSocketTicket({
|
|
1499
|
-
trigger: config.watchSecret,
|
|
1500
|
-
replay: config.replay
|
|
1501
|
-
});
|
|
1502
|
-
return Response.json({
|
|
1503
|
-
ticket: issued.ticket,
|
|
1504
|
-
expires_at: new Date(issued.expiresAt * 1e3).toISOString()
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
async function offeredSecret(request) {
|
|
1508
|
-
const body = await request.json().catch(() => null);
|
|
1509
|
-
return typeof body?.secret === "string" ? body.secret : "";
|
|
1510
|
-
}
|
|
1511
1191
|
export {
|
|
1512
1192
|
askWallet,
|
|
1513
|
-
blindLightningRail,
|
|
1514
|
-
invoiceFrom,
|
|
1515
|
-
lightningVerifyEndpoint,
|
|
1516
|
-
lnurlPayEndpoint,
|
|
1517
1193
|
nwcConnection,
|
|
1518
1194
|
nwcHoldInvoice,
|
|
1519
1195
|
nwcInvoice,
|
|
@@ -1521,8 +1197,5 @@ export {
|
|
|
1521
1197
|
nwcRail,
|
|
1522
1198
|
nwcSettlement,
|
|
1523
1199
|
nwcVerifyEndpoint,
|
|
1524
|
-
nwcVerifyUrl
|
|
1525
|
-
publicWatchTicketEndpoint,
|
|
1526
|
-
relayedVerifyUrl,
|
|
1527
|
-
watchTicketEndpoint
|
|
1200
|
+
nwcVerifyUrl
|
|
1528
1201
|
};
|