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