thunder-bridge 0.8.1 → 0.8.2
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 +36 -12
- package/dist/index.cjs +312 -90
- package/dist/index.d.cts +142 -14
- package/dist/index.d.ts +142 -14
- package/dist/index.js +307 -90
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -34,10 +34,8 @@ var ProblemError = class extends Error {
|
|
|
34
34
|
this.detail = problem.detail ?? null;
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
var NAMESPACE = "urn:problem-type:thunder-bridge:";
|
|
38
|
-
var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
|
|
39
37
|
function isProblemType(problem, type) {
|
|
40
|
-
return problem.type === type
|
|
38
|
+
return problem.type === type;
|
|
41
39
|
}
|
|
42
40
|
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
43
41
|
var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
@@ -73,8 +71,8 @@ function hexToBytes(hex) {
|
|
|
73
71
|
}
|
|
74
72
|
return bytes;
|
|
75
73
|
}
|
|
76
|
-
function isHex(
|
|
77
|
-
return /^[0-9a-f]+$/i.test(
|
|
74
|
+
function isHex(text3) {
|
|
75
|
+
return /^[0-9a-f]+$/i.test(text3) && text3.length % 2 === 0;
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
// ../core/sha256.ts
|
|
@@ -168,8 +166,8 @@ function sha256(bytes) {
|
|
|
168
166
|
}
|
|
169
167
|
return asBytes(state);
|
|
170
168
|
}
|
|
171
|
-
function sha256Hex(
|
|
172
|
-
return bytesToHex(sha256(new TextEncoder().encode(
|
|
169
|
+
function sha256Hex(text3) {
|
|
170
|
+
return bytesToHex(sha256(new TextEncoder().encode(text3)));
|
|
173
171
|
}
|
|
174
172
|
function withPadding(bytes) {
|
|
175
173
|
const padded = new Uint8Array(
|
|
@@ -348,10 +346,13 @@ function sameOrigin(one, other) {
|
|
|
348
346
|
return false;
|
|
349
347
|
}
|
|
350
348
|
}
|
|
349
|
+
function publicAddress(literal) {
|
|
350
|
+
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
351
|
+
}
|
|
351
352
|
function hostIsPublic(host) {
|
|
352
|
-
if (host.startsWith("[")) return
|
|
353
|
+
if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
|
|
353
354
|
const name = host.replace(/\.$/, "");
|
|
354
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return
|
|
355
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
|
|
355
356
|
const labels = name.split(".");
|
|
356
357
|
if (labels.length < 2 || labels.some((label) => label === "")) return false;
|
|
357
358
|
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
@@ -616,6 +617,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
|
|
|
616
617
|
var RECONNECT_DELAY_MS = 3e3;
|
|
617
618
|
var RECONNECT_CAP_MS = 3e4;
|
|
618
619
|
var UNANSWERED_ATTEMPTS = 5;
|
|
620
|
+
var TRIGGER_MIN_CHARS = 16;
|
|
621
|
+
var STRANGER_PROBE = "is-this-gateway-yours";
|
|
622
|
+
function unguessable(trigger) {
|
|
623
|
+
if (trigger.length < TRIGGER_MIN_CHARS) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`a trigger secret is the only thing guarding its stream, and nothing rate limits a guess at it, so it has to be at least ${TRIGGER_MIN_CHARS} characters nobody can predict`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
return trigger;
|
|
629
|
+
}
|
|
619
630
|
function backoffMs(firstDelay, attempt) {
|
|
620
631
|
const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
|
|
621
632
|
return Math.round(grown * (0.5 + Math.random() / 2));
|
|
@@ -624,6 +635,7 @@ var ThunderBridge = class {
|
|
|
624
635
|
baseUrl;
|
|
625
636
|
verify;
|
|
626
637
|
token;
|
|
638
|
+
strangers = null;
|
|
627
639
|
constructor(baseUrl, options) {
|
|
628
640
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
629
641
|
this.verify = options?.verify ?? true;
|
|
@@ -637,6 +649,21 @@ var ThunderBridge = class {
|
|
|
637
649
|
get isPrivate() {
|
|
638
650
|
return this.token !== null;
|
|
639
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Whether the gateway turns away a caller carrying no token, asked by making
|
|
654
|
+
* one unauthenticated read it would have to refuse. `isPrivate` answers only
|
|
655
|
+
* whether you configured a token, which is your side of the arrangement and
|
|
656
|
+
* says nothing about the gateway's, so a made-up token against a public
|
|
657
|
+
* instance reads as private and is not. Asked once and remembered, because an
|
|
658
|
+
* instance does not change its mind. Anything other than a refusal counts as
|
|
659
|
+
* open, so an unreachable gateway fails closed
|
|
660
|
+
*/
|
|
661
|
+
async refusesStrangers() {
|
|
662
|
+
this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
|
|
663
|
+
headers: { accept: "application/json" }
|
|
664
|
+
}).then((answer) => answer.status === 401).catch(() => false);
|
|
665
|
+
return await this.strangers;
|
|
666
|
+
}
|
|
640
667
|
/**
|
|
641
668
|
* Ask the gateway for an invoice payable to the first address on your list
|
|
642
669
|
* that can issue a provable one, throws `NoWalletAvailableError` when none can
|
|
@@ -648,7 +675,10 @@ var ThunderBridge = class {
|
|
|
648
675
|
const response = await fetch(`${this.baseUrl}/incoming-payments`, {
|
|
649
676
|
method: "POST",
|
|
650
677
|
headers,
|
|
651
|
-
body: createRequestBody(
|
|
678
|
+
body: createRequestBody(
|
|
679
|
+
params,
|
|
680
|
+
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
681
|
+
)
|
|
652
682
|
});
|
|
653
683
|
if (!response.ok) throw await problemFrom(response);
|
|
654
684
|
const payment = await paymentFrom(response);
|
|
@@ -890,7 +920,10 @@ var ThunderBridge = class {
|
|
|
890
920
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
891
921
|
method: "POST",
|
|
892
922
|
headers: this.sending(),
|
|
893
|
-
body: watchRequestBody(
|
|
923
|
+
body: watchRequestBody(
|
|
924
|
+
params,
|
|
925
|
+
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
926
|
+
)
|
|
894
927
|
});
|
|
895
928
|
if (!response.ok) throw await problemFrom(response);
|
|
896
929
|
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
@@ -909,7 +942,7 @@ var ThunderBridge = class {
|
|
|
909
942
|
*/
|
|
910
943
|
followTrigger(secret, options) {
|
|
911
944
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
912
|
-
const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
|
|
945
|
+
const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
|
|
913
946
|
const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
|
|
914
947
|
let socket = null;
|
|
915
948
|
let retry;
|
|
@@ -1081,10 +1114,10 @@ function toBase64Url(bytes) {
|
|
|
1081
1114
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1082
1115
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1083
1116
|
}
|
|
1084
|
-
function fromBase64Url(
|
|
1085
|
-
if (
|
|
1117
|
+
function fromBase64Url(text3) {
|
|
1118
|
+
if (text3.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text3)) return null;
|
|
1086
1119
|
try {
|
|
1087
|
-
const binary = atob(
|
|
1120
|
+
const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1088
1121
|
const bytes = new Uint8Array(binary.length);
|
|
1089
1122
|
for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
|
|
1090
1123
|
return bytes;
|
|
@@ -1116,6 +1149,87 @@ function asBuffer(bytes) {
|
|
|
1116
1149
|
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1117
1150
|
}
|
|
1118
1151
|
|
|
1152
|
+
// ../core/outbound.ts
|
|
1153
|
+
import { lookup } from "dns/promises";
|
|
1154
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
1155
|
+
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1156
|
+
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
1157
|
+
var HOPS_FOLLOWED = 2;
|
|
1158
|
+
var REDIRECTS = [301, 302, 303, 307, 308];
|
|
1159
|
+
var KEEPS_THE_METHOD = [307, 308];
|
|
1160
|
+
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
1161
|
+
var BODY_LIMIT_BYTES = 262144;
|
|
1162
|
+
async function ask(url, sent = {}) {
|
|
1163
|
+
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
|
|
1164
|
+
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
1165
|
+
let target = url;
|
|
1166
|
+
let carried = sent;
|
|
1167
|
+
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
1168
|
+
await refuseUnlessPublic(target);
|
|
1169
|
+
const response = await fetch(target, {
|
|
1170
|
+
method: carried.method ?? "GET",
|
|
1171
|
+
headers: carried.headers ?? {},
|
|
1172
|
+
body: carried.body,
|
|
1173
|
+
redirect: "manual",
|
|
1174
|
+
signal
|
|
1175
|
+
});
|
|
1176
|
+
if (!REDIRECTS.includes(response.status)) return await answerOf(response);
|
|
1177
|
+
const location = response.headers.get("location");
|
|
1178
|
+
if (location === null) {
|
|
1179
|
+
throw new Error(`${target} answered ${response.status} without saying where to`);
|
|
1180
|
+
}
|
|
1181
|
+
await response.body?.cancel();
|
|
1182
|
+
const next = new URL(location, target).toString();
|
|
1183
|
+
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
1184
|
+
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
1185
|
+
target = next;
|
|
1186
|
+
}
|
|
1187
|
+
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
1188
|
+
}
|
|
1189
|
+
async function resolvesNothingPrivate(url) {
|
|
1190
|
+
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
1191
|
+
const found = await Promise.race([
|
|
1192
|
+
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
1193
|
+
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
1194
|
+
]);
|
|
1195
|
+
return found !== null && found.every((one) => publicAddress(one.address));
|
|
1196
|
+
}
|
|
1197
|
+
async function refuseUnlessPublic(url) {
|
|
1198
|
+
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
1199
|
+
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
1200
|
+
}
|
|
1201
|
+
async function answerOf(response) {
|
|
1202
|
+
const { status, ok } = response;
|
|
1203
|
+
const reader = response.body?.getReader();
|
|
1204
|
+
if (!reader) return { status, ok, body: "", truncated: false };
|
|
1205
|
+
const read = [];
|
|
1206
|
+
let bytes = 0;
|
|
1207
|
+
while (bytes <= BODY_LIMIT_BYTES) {
|
|
1208
|
+
const { done, value } = await reader.read();
|
|
1209
|
+
if (done) return { status, ok, body: text2(read), truncated: false };
|
|
1210
|
+
read.push(value);
|
|
1211
|
+
bytes += value.length;
|
|
1212
|
+
}
|
|
1213
|
+
await reader.cancel();
|
|
1214
|
+
return { status, ok, body: text2(read), truncated: true };
|
|
1215
|
+
}
|
|
1216
|
+
function withoutBody(sent) {
|
|
1217
|
+
const headers = { ...sent.headers };
|
|
1218
|
+
delete headers["content-type"];
|
|
1219
|
+
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1220
|
+
}
|
|
1221
|
+
function withoutCredentials(sent) {
|
|
1222
|
+
const headers = { ...sent.headers };
|
|
1223
|
+
for (const named of CREDENTIALS) delete headers[named];
|
|
1224
|
+
return { ...sent, headers };
|
|
1225
|
+
}
|
|
1226
|
+
function text2(chunks) {
|
|
1227
|
+
const decoder = new TextDecoder();
|
|
1228
|
+
let decoded = "";
|
|
1229
|
+
for (const chunk of chunks) decoded += decoder.decode(chunk, { stream: true });
|
|
1230
|
+
return decoded + decoder.decode();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1119
1233
|
// ../core/refusal.ts
|
|
1120
1234
|
var WalletRefused = class extends Error {
|
|
1121
1235
|
reason;
|
|
@@ -1134,7 +1248,6 @@ var NoWalletAvailable = class extends Error {
|
|
|
1134
1248
|
|
|
1135
1249
|
// ../core/lnurl.ts
|
|
1136
1250
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1137
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1138
1251
|
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1139
1252
|
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1140
1253
|
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
@@ -1299,14 +1412,12 @@ function splitAddress(address) {
|
|
|
1299
1412
|
return [user, domain];
|
|
1300
1413
|
}
|
|
1301
1414
|
async function fetchJson(url, deadline) {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (!response.ok) throw new Error(`${url} answered ${response.status}`);
|
|
1309
|
-
return await response.json();
|
|
1415
|
+
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
1416
|
+
if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
|
|
1417
|
+
if (answer.truncated) {
|
|
1418
|
+
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
1419
|
+
}
|
|
1420
|
+
return JSON.parse(answer.body);
|
|
1310
1421
|
}
|
|
1311
1422
|
|
|
1312
1423
|
// src/trigger.ts
|
|
@@ -1450,6 +1561,7 @@ var FORBIDDEN_IN_SPD = "*";
|
|
|
1450
1561
|
async function bankTransfer(params) {
|
|
1451
1562
|
const currency = params.currency ?? DEFAULT_CURRENCY;
|
|
1452
1563
|
refuseUnusable(params, currency);
|
|
1564
|
+
await refuseAnOpenGateway(params);
|
|
1453
1565
|
const spd = shortPaymentDescriptor(params, currency);
|
|
1454
1566
|
const subject = subjectOf(params.reference, params.amountMinor, currency);
|
|
1455
1567
|
const polling = new URL(params.verifyUrl);
|
|
@@ -1516,12 +1628,14 @@ function shortPaymentDescriptor(params, currency) {
|
|
|
1516
1628
|
function major(amountMinor, currency) {
|
|
1517
1629
|
return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
|
|
1518
1630
|
}
|
|
1631
|
+
async function refuseAnOpenGateway(params) {
|
|
1632
|
+
if (params.allowPublicGateway === true) return;
|
|
1633
|
+
if (await params.gateway.refusesStrangers()) return;
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
"this gateway serves callers with no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Point at one that answers 401 to a stranger, or say allowPublicGateway"
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1519
1638
|
function refuseUnusable(params, currency) {
|
|
1520
|
-
if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
|
|
1521
|
-
throw new Error(
|
|
1522
|
-
"this gateway has no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Give it a token or say allowPublicGateway"
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
1639
|
if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
|
|
1526
1640
|
if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
|
|
1527
1641
|
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
@@ -1560,6 +1674,7 @@ var WHERE_A_PAYER_WRITES = [
|
|
|
1560
1674
|
USER_IDENTIFICATION,
|
|
1561
1675
|
PAYER_REFERENCE
|
|
1562
1676
|
];
|
|
1677
|
+
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
1563
1678
|
function fioStatement(config) {
|
|
1564
1679
|
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
1565
1680
|
if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
|
|
@@ -1612,9 +1727,21 @@ function asCredit(transaction) {
|
|
|
1612
1727
|
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
1613
1728
|
currency: textIn(transaction[CURRENCY]),
|
|
1614
1729
|
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
1615
|
-
bookedAt:
|
|
1730
|
+
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
1616
1731
|
};
|
|
1617
1732
|
}
|
|
1733
|
+
function bookedAtIn(cell) {
|
|
1734
|
+
const value = cell?.value;
|
|
1735
|
+
if (typeof value === "number") return Math.floor(value / MILLIS);
|
|
1736
|
+
const at = Date.parse(asIso8601(textIn(cell)));
|
|
1737
|
+
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
1738
|
+
}
|
|
1739
|
+
function asIso8601(booked) {
|
|
1740
|
+
const day = booked.slice(0, DATE_CHARS);
|
|
1741
|
+
const zone = booked.slice(DATE_CHARS);
|
|
1742
|
+
if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
|
|
1743
|
+
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
1744
|
+
}
|
|
1618
1745
|
function numberIn(cell) {
|
|
1619
1746
|
const value = Number(cell?.value ?? Number.NaN);
|
|
1620
1747
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -1627,6 +1754,149 @@ function asDate(unix) {
|
|
|
1627
1754
|
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
1628
1755
|
}
|
|
1629
1756
|
|
|
1757
|
+
// src/qr.ts
|
|
1758
|
+
import { encode } from "uqr";
|
|
1759
|
+
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1760
|
+
function encodeForQr(destination) {
|
|
1761
|
+
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1762
|
+
return `LIGHTNING:${scanned}`;
|
|
1763
|
+
}
|
|
1764
|
+
function invoiceToSvg(destination, options) {
|
|
1765
|
+
return svgOf(encodeForQr(destination), options);
|
|
1766
|
+
}
|
|
1767
|
+
function invoiceToDataUrl(destination, options) {
|
|
1768
|
+
return asDataUrl(invoiceToSvg(destination, options));
|
|
1769
|
+
}
|
|
1770
|
+
function lnurlToSvg(endpoint, options) {
|
|
1771
|
+
return svgOf(toLnurl(endpoint), options);
|
|
1772
|
+
}
|
|
1773
|
+
function lnurlToDataUrl(endpoint, options) {
|
|
1774
|
+
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1775
|
+
}
|
|
1776
|
+
function spdToSvg(spd, options) {
|
|
1777
|
+
return svgOf(spd, options);
|
|
1778
|
+
}
|
|
1779
|
+
function spdToDataUrl(spd, options) {
|
|
1780
|
+
return asDataUrl(spdToSvg(spd, options));
|
|
1781
|
+
}
|
|
1782
|
+
function qrToSvg(payload, options) {
|
|
1783
|
+
return svgOf(payload, options);
|
|
1784
|
+
}
|
|
1785
|
+
function qrToDataUrl(payload, options) {
|
|
1786
|
+
return asDataUrl(qrToSvg(payload, options));
|
|
1787
|
+
}
|
|
1788
|
+
function svgOf(scanned, options) {
|
|
1789
|
+
const size = options?.size ?? 256;
|
|
1790
|
+
const color = options?.color ?? "#000";
|
|
1791
|
+
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1792
|
+
return renderSvg(scanned, size, color);
|
|
1793
|
+
}
|
|
1794
|
+
function asDataUrl(svg) {
|
|
1795
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1796
|
+
}
|
|
1797
|
+
function isLnAddress(destination) {
|
|
1798
|
+
const at = destination.indexOf("@");
|
|
1799
|
+
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1800
|
+
}
|
|
1801
|
+
function renderSvg(content, size, color) {
|
|
1802
|
+
const { data } = encode(content);
|
|
1803
|
+
const modules = data.length;
|
|
1804
|
+
const cellSize = size / modules;
|
|
1805
|
+
let path = "";
|
|
1806
|
+
for (let y = 0; y < modules; y++) {
|
|
1807
|
+
for (let x = 0; x < modules; x++) {
|
|
1808
|
+
if (data[y][x]) {
|
|
1809
|
+
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return [
|
|
1814
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1815
|
+
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1816
|
+
`<path d="${path}" fill="${color}"/>`,
|
|
1817
|
+
`</svg>`
|
|
1818
|
+
].join("");
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/rail.ts
|
|
1822
|
+
var BANK = "bank";
|
|
1823
|
+
var LIGHTNING = "lightning";
|
|
1824
|
+
function bankRail(config) {
|
|
1825
|
+
return async (order) => {
|
|
1826
|
+
const expiresAt = config.expiresAt(order);
|
|
1827
|
+
const transfer = await bankTransfer({
|
|
1828
|
+
gateway: config.gateway,
|
|
1829
|
+
secret: config.secret,
|
|
1830
|
+
iban: config.iban,
|
|
1831
|
+
verifyUrl: config.verifyUrl,
|
|
1832
|
+
reference: order.reference,
|
|
1833
|
+
amountMinor: order.amountMinor,
|
|
1834
|
+
currency: order.currency,
|
|
1835
|
+
expiresAt,
|
|
1836
|
+
trigger: config.trigger,
|
|
1837
|
+
sealed: await config.sealed?.(order),
|
|
1838
|
+
variableSymbol: config.variableSymbol?.(order),
|
|
1839
|
+
allowPublicGateway: config.allowPublicGateway
|
|
1840
|
+
});
|
|
1841
|
+
return {
|
|
1842
|
+
id: transfer.id,
|
|
1843
|
+
rail: config.name ?? BANK,
|
|
1844
|
+
scan: transfer.spd,
|
|
1845
|
+
qr: transfer.spd,
|
|
1846
|
+
expiresAt
|
|
1847
|
+
};
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
function lightningRail(config) {
|
|
1851
|
+
return async (order) => {
|
|
1852
|
+
const payment = await config.gateway.createPayment(
|
|
1853
|
+
{
|
|
1854
|
+
lnAddresses: config.lnAddresses,
|
|
1855
|
+
amountMsat: await config.amountMsat(order),
|
|
1856
|
+
webhookUrl: config.webhookUrl,
|
|
1857
|
+
webhookSecret: config.webhookSecret
|
|
1858
|
+
},
|
|
1859
|
+
{ idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
|
|
1860
|
+
);
|
|
1861
|
+
return {
|
|
1862
|
+
id: payment.id,
|
|
1863
|
+
rail: config.name ?? LIGHTNING,
|
|
1864
|
+
scan: payment.bolt11,
|
|
1865
|
+
qr: encodeForQr(payment.bolt11),
|
|
1866
|
+
expiresAt: payment.expiresAt
|
|
1867
|
+
};
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function blindLightningRail(config) {
|
|
1871
|
+
return async (order) => {
|
|
1872
|
+
const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
1873
|
+
const watched = await config.gateway.watchPayment({
|
|
1874
|
+
paymentHash: resolved.paymentHash,
|
|
1875
|
+
verifyUrl: resolved.verifyUrl,
|
|
1876
|
+
expiresAt: resolved.expiresAt,
|
|
1877
|
+
trigger: config.trigger,
|
|
1878
|
+
sealed: await config.sealed?.(order)
|
|
1879
|
+
});
|
|
1880
|
+
return {
|
|
1881
|
+
id: watched.id,
|
|
1882
|
+
rail: config.name ?? LIGHTNING,
|
|
1883
|
+
scan: resolved.bolt11,
|
|
1884
|
+
qr: encodeForQr(resolved.bolt11),
|
|
1885
|
+
expiresAt: resolved.expiresAt
|
|
1886
|
+
};
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
async function invoiceFrom(lnAddresses, amountMsat) {
|
|
1890
|
+
try {
|
|
1891
|
+
return await resolve(lnAddresses, amountMsat);
|
|
1892
|
+
} catch (refused) {
|
|
1893
|
+
if (refused instanceof NoWalletAvailable) {
|
|
1894
|
+
throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
|
|
1895
|
+
}
|
|
1896
|
+
throw refused;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1630
1900
|
// src/price.ts
|
|
1631
1901
|
var MSAT_PER_BTC2 = 1e11;
|
|
1632
1902
|
var BASIS_POINTS = 1e4;
|
|
@@ -1749,64 +2019,6 @@ function refusals2(answers) {
|
|
|
1749
2019
|
).join("; ");
|
|
1750
2020
|
}
|
|
1751
2021
|
|
|
1752
|
-
// src/qr.ts
|
|
1753
|
-
import { encode } from "uqr";
|
|
1754
|
-
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1755
|
-
function encodeForQr(destination) {
|
|
1756
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1757
|
-
return `LIGHTNING:${scanned}`;
|
|
1758
|
-
}
|
|
1759
|
-
function invoiceToSvg(destination, options) {
|
|
1760
|
-
return svgOf(encodeForQr(destination), options);
|
|
1761
|
-
}
|
|
1762
|
-
function invoiceToDataUrl(destination, options) {
|
|
1763
|
-
return asDataUrl(invoiceToSvg(destination, options));
|
|
1764
|
-
}
|
|
1765
|
-
function lnurlToSvg(endpoint, options) {
|
|
1766
|
-
return svgOf(toLnurl(endpoint), options);
|
|
1767
|
-
}
|
|
1768
|
-
function lnurlToDataUrl(endpoint, options) {
|
|
1769
|
-
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1770
|
-
}
|
|
1771
|
-
function spdToSvg(spd, options) {
|
|
1772
|
-
return svgOf(spd, options);
|
|
1773
|
-
}
|
|
1774
|
-
function spdToDataUrl(spd, options) {
|
|
1775
|
-
return asDataUrl(spdToSvg(spd, options));
|
|
1776
|
-
}
|
|
1777
|
-
function svgOf(scanned, options) {
|
|
1778
|
-
const size = options?.size ?? 256;
|
|
1779
|
-
const color = options?.color ?? "#000";
|
|
1780
|
-
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1781
|
-
return renderSvg(scanned, size, color);
|
|
1782
|
-
}
|
|
1783
|
-
function asDataUrl(svg) {
|
|
1784
|
-
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1785
|
-
}
|
|
1786
|
-
function isLnAddress(destination) {
|
|
1787
|
-
const at = destination.indexOf("@");
|
|
1788
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1789
|
-
}
|
|
1790
|
-
function renderSvg(content, size, color) {
|
|
1791
|
-
const { data } = encode(content);
|
|
1792
|
-
const modules = data.length;
|
|
1793
|
-
const cellSize = size / modules;
|
|
1794
|
-
let path = "";
|
|
1795
|
-
for (let y = 0; y < modules; y++) {
|
|
1796
|
-
for (let x = 0; x < modules; x++) {
|
|
1797
|
-
if (data[y][x]) {
|
|
1798
|
-
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
return [
|
|
1803
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1804
|
-
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1805
|
-
`<path d="${path}" fill="${color}"/>`,
|
|
1806
|
-
`</svg>`
|
|
1807
|
-
].join("");
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
2022
|
// src/webhook.ts
|
|
1811
2023
|
var SIGNATURE_HEADER = "x-signature";
|
|
1812
2024
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
@@ -1819,9 +2031,9 @@ async function verifyWebhookSignature(body, signature, secret, timestamp, option
|
|
|
1819
2031
|
}
|
|
1820
2032
|
async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
1821
2033
|
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
1822
|
-
const
|
|
2034
|
+
const text3 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1823
2035
|
try {
|
|
1824
|
-
return paymentFromWire(JSON.parse(
|
|
2036
|
+
return paymentFromWire(JSON.parse(text3));
|
|
1825
2037
|
} catch {
|
|
1826
2038
|
return null;
|
|
1827
2039
|
}
|
|
@@ -1860,9 +2072,11 @@ export {
|
|
|
1860
2072
|
REQUEST_IN_FLIGHT,
|
|
1861
2073
|
ThunderBridge,
|
|
1862
2074
|
UnverifiedRecipientError,
|
|
2075
|
+
bankRail,
|
|
1863
2076
|
bankTransfer,
|
|
1864
2077
|
bankVerifyEndpoint,
|
|
1865
2078
|
bitstamp,
|
|
2079
|
+
blindLightningRail,
|
|
1866
2080
|
coinbase,
|
|
1867
2081
|
coinmate,
|
|
1868
2082
|
decodeInvoice,
|
|
@@ -1872,6 +2086,7 @@ export {
|
|
|
1872
2086
|
isProblemType,
|
|
1873
2087
|
isProvablyPaid,
|
|
1874
2088
|
kraken,
|
|
2089
|
+
lightningRail,
|
|
1875
2090
|
lnurlPayEndpoint,
|
|
1876
2091
|
lnurlToDataUrl,
|
|
1877
2092
|
lnurlToSvg,
|
|
@@ -1884,6 +2099,8 @@ export {
|
|
|
1884
2099
|
preimageMatchesHash,
|
|
1885
2100
|
proveOrigin,
|
|
1886
2101
|
proveSettlement,
|
|
2102
|
+
qrToDataUrl,
|
|
2103
|
+
qrToSvg,
|
|
1887
2104
|
seal,
|
|
1888
2105
|
spdToDataUrl,
|
|
1889
2106
|
spdToSvg,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Trustless JavaScript client for the Thunder Bridge Lightning payment gateway. Proves the invoice came from your own wallet before the payer sees it.",
|
|
5
5
|
"author": "i-am-fatik",
|
|
6
6
|
"homepage": "https://agora.gripe/en/tools/thunder-bridge",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@biomejs/biome": "^2.4.16",
|
|
42
|
-
"@types/node": "^
|
|
42
|
+
"@types/node": "^24.13.3",
|
|
43
43
|
"tsup": "^8.0.0",
|
|
44
44
|
"typescript": "^5.5.0",
|
|
45
|
-
"vitest": "^
|
|
45
|
+
"vitest": "^4.1.10"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"lightning",
|