fulgur-bridge-client 0.0.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 +255 -0
- package/dist/index.cjs +473 -0
- package/dist/index.d.cts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +426 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
RestGatewayClient: () => RestGatewayClient,
|
|
34
|
+
detectDestinationType: () => detectDestinationType,
|
|
35
|
+
invoiceToDataUrl: () => invoiceToDataUrl,
|
|
36
|
+
invoiceToSvg: () => invoiceToSvg,
|
|
37
|
+
isBolt12Offer: () => isBolt12Offer,
|
|
38
|
+
isLnAddress: () => isLnAddress,
|
|
39
|
+
parseWebhook: () => parseWebhook,
|
|
40
|
+
parseWebhookRequest: () => parseWebhookRequest,
|
|
41
|
+
solvePow: () => solvePow,
|
|
42
|
+
verifyPow: () => verifyPow,
|
|
43
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/pow.ts
|
|
48
|
+
function leadingZeroBits(hash) {
|
|
49
|
+
let count = 0;
|
|
50
|
+
for (const byte of hash) {
|
|
51
|
+
if (byte === 0) {
|
|
52
|
+
count += 8;
|
|
53
|
+
} else {
|
|
54
|
+
count += Math.clz32(byte) - 24;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
var _nodeCrypto;
|
|
61
|
+
async function getNodeCrypto() {
|
|
62
|
+
if (_nodeCrypto !== void 0) return _nodeCrypto;
|
|
63
|
+
try {
|
|
64
|
+
_nodeCrypto = await import("crypto");
|
|
65
|
+
return _nodeCrypto;
|
|
66
|
+
} catch {
|
|
67
|
+
_nodeCrypto = null;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function solveWithNodeCrypto(crypto2, challenge, difficulty) {
|
|
72
|
+
const prefix = `${challenge}:`;
|
|
73
|
+
for (let nonce = 0; ; nonce++) {
|
|
74
|
+
const hash = crypto2.createHash("sha256").update(`${prefix}${nonce}`).digest();
|
|
75
|
+
if (leadingZeroBits(hash) >= difficulty) return nonce;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var K = new Uint32Array([
|
|
79
|
+
1116352408,
|
|
80
|
+
1899447441,
|
|
81
|
+
3049323471,
|
|
82
|
+
3921009573,
|
|
83
|
+
961987163,
|
|
84
|
+
1508970993,
|
|
85
|
+
2453635748,
|
|
86
|
+
2870763221,
|
|
87
|
+
3624381080,
|
|
88
|
+
310598401,
|
|
89
|
+
607225278,
|
|
90
|
+
1426881987,
|
|
91
|
+
1925078388,
|
|
92
|
+
2162078206,
|
|
93
|
+
2614888103,
|
|
94
|
+
3248222580,
|
|
95
|
+
3835390401,
|
|
96
|
+
4022224774,
|
|
97
|
+
264347078,
|
|
98
|
+
604807628,
|
|
99
|
+
770255983,
|
|
100
|
+
1249150122,
|
|
101
|
+
1555081692,
|
|
102
|
+
1996064986,
|
|
103
|
+
2554220882,
|
|
104
|
+
2821834349,
|
|
105
|
+
2952996808,
|
|
106
|
+
3210313671,
|
|
107
|
+
3336571891,
|
|
108
|
+
3584528711,
|
|
109
|
+
113926993,
|
|
110
|
+
338241895,
|
|
111
|
+
666307205,
|
|
112
|
+
773529912,
|
|
113
|
+
1294757372,
|
|
114
|
+
1396182291,
|
|
115
|
+
1695183700,
|
|
116
|
+
1986661051,
|
|
117
|
+
2177026350,
|
|
118
|
+
2456956037,
|
|
119
|
+
2730485921,
|
|
120
|
+
2820302411,
|
|
121
|
+
3259730800,
|
|
122
|
+
3345764771,
|
|
123
|
+
3516065817,
|
|
124
|
+
3600352804,
|
|
125
|
+
4094571909,
|
|
126
|
+
275423344,
|
|
127
|
+
430227734,
|
|
128
|
+
506948616,
|
|
129
|
+
659060556,
|
|
130
|
+
883997877,
|
|
131
|
+
958139571,
|
|
132
|
+
1322822218,
|
|
133
|
+
1537002063,
|
|
134
|
+
1747873779,
|
|
135
|
+
1955562222,
|
|
136
|
+
2024104815,
|
|
137
|
+
2227730452,
|
|
138
|
+
2361852424,
|
|
139
|
+
2428436474,
|
|
140
|
+
2756734187,
|
|
141
|
+
3204031479,
|
|
142
|
+
3329325298
|
|
143
|
+
]);
|
|
144
|
+
function sha256js(bytes) {
|
|
145
|
+
const len = bytes.length;
|
|
146
|
+
const bitLen = len * 8;
|
|
147
|
+
const padLen = len + 9 + 63 & ~63;
|
|
148
|
+
const buf = new Uint8Array(padLen);
|
|
149
|
+
buf.set(bytes);
|
|
150
|
+
buf[len] = 128;
|
|
151
|
+
const dv = new DataView(buf.buffer);
|
|
152
|
+
dv.setUint32(padLen - 4, bitLen, false);
|
|
153
|
+
let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762;
|
|
154
|
+
let h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
|
|
155
|
+
const w = new Uint32Array(64);
|
|
156
|
+
for (let off = 0; off < padLen; off += 64) {
|
|
157
|
+
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
|
|
158
|
+
for (let i = 16; i < 64; i++) {
|
|
159
|
+
const s0 = (w[i - 15] >>> 7 | w[i - 15] << 25) ^ (w[i - 15] >>> 18 | w[i - 15] << 14) ^ w[i - 15] >>> 3;
|
|
160
|
+
const s1 = (w[i - 2] >>> 17 | w[i - 2] << 15) ^ (w[i - 2] >>> 19 | w[i - 2] << 13) ^ w[i - 2] >>> 10;
|
|
161
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
162
|
+
}
|
|
163
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
164
|
+
for (let i = 0; i < 64; i++) {
|
|
165
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
166
|
+
const ch = e & f ^ ~e & g;
|
|
167
|
+
const t1 = h + S1 + ch + K[i] + w[i] | 0;
|
|
168
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
169
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
170
|
+
const t2 = S0 + maj | 0;
|
|
171
|
+
h = g;
|
|
172
|
+
g = f;
|
|
173
|
+
f = e;
|
|
174
|
+
e = d + t1 | 0;
|
|
175
|
+
d = c;
|
|
176
|
+
c = b;
|
|
177
|
+
b = a;
|
|
178
|
+
a = t1 + t2 | 0;
|
|
179
|
+
}
|
|
180
|
+
h0 = h0 + a | 0;
|
|
181
|
+
h1 = h1 + b | 0;
|
|
182
|
+
h2 = h2 + c | 0;
|
|
183
|
+
h3 = h3 + d | 0;
|
|
184
|
+
h4 = h4 + e | 0;
|
|
185
|
+
h5 = h5 + f | 0;
|
|
186
|
+
h6 = h6 + g | 0;
|
|
187
|
+
h7 = h7 + h | 0;
|
|
188
|
+
}
|
|
189
|
+
const out = new Uint8Array(32);
|
|
190
|
+
const odv = new DataView(out.buffer);
|
|
191
|
+
odv.setUint32(0, h0);
|
|
192
|
+
odv.setUint32(4, h1);
|
|
193
|
+
odv.setUint32(8, h2);
|
|
194
|
+
odv.setUint32(12, h3);
|
|
195
|
+
odv.setUint32(16, h4);
|
|
196
|
+
odv.setUint32(20, h5);
|
|
197
|
+
odv.setUint32(24, h6);
|
|
198
|
+
odv.setUint32(28, h7);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function solveWithPureJs(challenge, difficulty) {
|
|
202
|
+
const encoder = new TextEncoder();
|
|
203
|
+
const prefix = encoder.encode(`${challenge}:`);
|
|
204
|
+
for (let nonce = 0; ; nonce++) {
|
|
205
|
+
const suffix = encoder.encode(String(nonce));
|
|
206
|
+
const msg = new Uint8Array(prefix.length + suffix.length);
|
|
207
|
+
msg.set(prefix);
|
|
208
|
+
msg.set(suffix, prefix.length);
|
|
209
|
+
if (leadingZeroBits(sha256js(msg)) >= difficulty) return nonce;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function solveWithSubtle(challenge, difficulty) {
|
|
213
|
+
const BATCH_SIZE = 5e3;
|
|
214
|
+
const encoder = new TextEncoder();
|
|
215
|
+
for (let start = 0; ; start += BATCH_SIZE) {
|
|
216
|
+
for (let nonce = start; nonce < start + BATCH_SIZE; nonce++) {
|
|
217
|
+
const data = encoder.encode(`${challenge}:${nonce}`);
|
|
218
|
+
const buffer = await crypto.subtle.digest("SHA-256", data);
|
|
219
|
+
if (leadingZeroBits(new Uint8Array(buffer)) >= difficulty) return nonce;
|
|
220
|
+
}
|
|
221
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
225
|
+
async function solvePow(challenge, difficulty) {
|
|
226
|
+
const nodeCrypto = await getNodeCrypto();
|
|
227
|
+
if (nodeCrypto) return solveWithNodeCrypto(nodeCrypto, challenge, difficulty);
|
|
228
|
+
if (!isBrowser) return solveWithPureJs(challenge, difficulty);
|
|
229
|
+
return solveWithSubtle(challenge, difficulty);
|
|
230
|
+
}
|
|
231
|
+
async function verifyPow(challenge, nonce, difficulty) {
|
|
232
|
+
const nodeCrypto = await getNodeCrypto();
|
|
233
|
+
if (nodeCrypto) {
|
|
234
|
+
const hash = nodeCrypto.createHash("sha256").update(`${challenge}:${nonce}`).digest();
|
|
235
|
+
return leadingZeroBits(hash) >= difficulty;
|
|
236
|
+
}
|
|
237
|
+
const encoder = new TextEncoder();
|
|
238
|
+
return leadingZeroBits(sha256js(encoder.encode(`${challenge}:${nonce}`))) >= difficulty;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/rest.ts
|
|
242
|
+
var RestGatewayClient = class {
|
|
243
|
+
baseUrl;
|
|
244
|
+
constructor(gatewayUrl) {
|
|
245
|
+
this.baseUrl = gatewayUrl.replace(/\/$/, "");
|
|
246
|
+
}
|
|
247
|
+
/** Returns null when the gateway doesn't require PoW for this IP. */
|
|
248
|
+
async getChallenge() {
|
|
249
|
+
const resp = await fetch(`${this.baseUrl}/api/v1/pow-challenge`);
|
|
250
|
+
if (resp.status === 400) return null;
|
|
251
|
+
if (!resp.ok) throw new Error(`Challenge request failed: ${resp.status}`);
|
|
252
|
+
return resp.json();
|
|
253
|
+
}
|
|
254
|
+
/** Create a donation. Solves PoW challenge if the gateway requires one. */
|
|
255
|
+
async createDonation(params) {
|
|
256
|
+
const challenge = await this.getChallenge();
|
|
257
|
+
let pow;
|
|
258
|
+
if (challenge) {
|
|
259
|
+
const nonce = await solvePow(challenge.challenge, challenge.difficulty);
|
|
260
|
+
pow = { challenge: challenge.challenge, nonce };
|
|
261
|
+
}
|
|
262
|
+
const body = {
|
|
263
|
+
ln_address: params.destination,
|
|
264
|
+
amount_msat: params.amountSat * 1e3
|
|
265
|
+
};
|
|
266
|
+
if (pow) body.pow = pow;
|
|
267
|
+
if (params.webhookUrl) body.webhook_url = params.webhookUrl;
|
|
268
|
+
if (params.webhookSecret) body.webhook_secret = params.webhookSecret;
|
|
269
|
+
const resp = await fetch(`${this.baseUrl}/api/v1/donations`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify(body)
|
|
273
|
+
});
|
|
274
|
+
if (!resp.ok) {
|
|
275
|
+
const err = await resp.json().catch(() => ({ error: "unknown" }));
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Create donation failed (${resp.status}): ${err.error || JSON.stringify(err)}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const data = await resp.json();
|
|
281
|
+
return {
|
|
282
|
+
id: data.id,
|
|
283
|
+
bolt11: data.bolt11,
|
|
284
|
+
paymentHash: data.payment_hash
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async getDonation(id) {
|
|
288
|
+
const resp = await fetch(`${this.baseUrl}/api/v1/donations/${id}`);
|
|
289
|
+
if (!resp.ok) {
|
|
290
|
+
const err = await resp.json().catch(() => ({ error: "unknown" }));
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Get donation failed (${resp.status}): ${err.error || JSON.stringify(err)}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return resp.json();
|
|
296
|
+
}
|
|
297
|
+
async listDonations(params) {
|
|
298
|
+
const query = new URLSearchParams();
|
|
299
|
+
if (params?.limit != null) query.set("limit", String(params.limit));
|
|
300
|
+
if (params?.offset != null) query.set("offset", String(params.offset));
|
|
301
|
+
const qs = query.toString();
|
|
302
|
+
const resp = await fetch(
|
|
303
|
+
`${this.baseUrl}/api/v1/donations${qs ? `?${qs}` : ""}`
|
|
304
|
+
);
|
|
305
|
+
if (!resp.ok) {
|
|
306
|
+
const err = await resp.json().catch(() => ({ error: "unknown" }));
|
|
307
|
+
throw new Error(
|
|
308
|
+
`List donations failed (${resp.status}): ${err.error || JSON.stringify(err)}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const data = await resp.json();
|
|
312
|
+
return data.donations;
|
|
313
|
+
}
|
|
314
|
+
async estimateFees(amountSat) {
|
|
315
|
+
const resp = await fetch(
|
|
316
|
+
`${this.baseUrl}/api/v1/estimate-fees?amount_msat=${amountSat * 1e3}`
|
|
317
|
+
);
|
|
318
|
+
if (!resp.ok) {
|
|
319
|
+
const err = await resp.json().catch(() => ({ error: "unknown" }));
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Fee estimate failed (${resp.status}): ${err.error || JSON.stringify(err)}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return resp.json();
|
|
325
|
+
}
|
|
326
|
+
/** Subscribe via WS, resolve on terminal status. */
|
|
327
|
+
waitForPayment(donationId, options) {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const wsUrl = this.baseUrl.replace(/^http/, "ws") + `/ws/donations/${donationId}`;
|
|
330
|
+
const ws = new WebSocket(wsUrl);
|
|
331
|
+
const TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "expired"]);
|
|
332
|
+
let settled = false;
|
|
333
|
+
let timer;
|
|
334
|
+
const settle = (fn) => {
|
|
335
|
+
if (!settled) {
|
|
336
|
+
settled = true;
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
fn();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
ws.addEventListener("message", (event) => {
|
|
342
|
+
const msg = JSON.parse(String(event.data));
|
|
343
|
+
const status = msg.status;
|
|
344
|
+
options?.onStatusChange?.(status);
|
|
345
|
+
if (TERMINAL.has(status)) {
|
|
346
|
+
settle(() => resolve(status));
|
|
347
|
+
ws.close();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
ws.addEventListener("error", () => {
|
|
351
|
+
settle(() => reject(new Error("WebSocket connection failed")));
|
|
352
|
+
});
|
|
353
|
+
ws.addEventListener("close", () => {
|
|
354
|
+
settle(() => reject(new Error("WebSocket closed before terminal status")));
|
|
355
|
+
});
|
|
356
|
+
if (options?.timeoutMs) {
|
|
357
|
+
timer = setTimeout(() => {
|
|
358
|
+
ws.close();
|
|
359
|
+
settle(() => reject(new Error("waitForPayment timed out")));
|
|
360
|
+
}, options.timeoutMs);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/qr.ts
|
|
367
|
+
var import_uqr = require("uqr");
|
|
368
|
+
|
|
369
|
+
// src/lightning.ts
|
|
370
|
+
function isBolt12Offer(s) {
|
|
371
|
+
return s.startsWith("lno1");
|
|
372
|
+
}
|
|
373
|
+
function isLnAddress(s) {
|
|
374
|
+
const at = s.indexOf("@");
|
|
375
|
+
if (at < 1) return false;
|
|
376
|
+
const domain = s.slice(at + 1);
|
|
377
|
+
return domain.includes(".") && !domain.startsWith(".") && !domain.endsWith(".");
|
|
378
|
+
}
|
|
379
|
+
function detectDestinationType(s) {
|
|
380
|
+
if (isBolt12Offer(s)) return "bolt12Offer";
|
|
381
|
+
if (isLnAddress(s)) return "lnAddress";
|
|
382
|
+
return "unknown";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/qr.ts
|
|
386
|
+
var SAFE_COLOR = /^#[0-9a-fA-F]{3,8}$|^rgb\(\d|^rgba\(\d|^[a-z]+$/;
|
|
387
|
+
function invoiceToSvg(invoice, options) {
|
|
388
|
+
const size = options?.size ?? 256;
|
|
389
|
+
const color = options?.color ?? "#000";
|
|
390
|
+
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
391
|
+
const withPrefix = options?.withPrefix ?? true;
|
|
392
|
+
let content;
|
|
393
|
+
if (withPrefix && !isBolt12Offer(invoice)) {
|
|
394
|
+
content = `LIGHTNING:${invoice.toUpperCase()}`;
|
|
395
|
+
} else {
|
|
396
|
+
content = invoice;
|
|
397
|
+
}
|
|
398
|
+
const { data } = (0, import_uqr.encode)(content);
|
|
399
|
+
const modules = data.length;
|
|
400
|
+
const cellSize = size / modules;
|
|
401
|
+
let path = "";
|
|
402
|
+
for (let y = 0; y < modules; y++) {
|
|
403
|
+
for (let x = 0; x < modules; x++) {
|
|
404
|
+
if (data[y][x]) {
|
|
405
|
+
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return [
|
|
410
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
411
|
+
`<rect width="100%" height="100%" fill="white"/>`,
|
|
412
|
+
`<path d="${path}" fill="${color}"/>`,
|
|
413
|
+
`</svg>`
|
|
414
|
+
].join("");
|
|
415
|
+
}
|
|
416
|
+
function invoiceToDataUrl(invoice, options) {
|
|
417
|
+
const svg = invoiceToSvg(invoice, options);
|
|
418
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/webhook.ts
|
|
422
|
+
var _crypto;
|
|
423
|
+
async function getNodeCrypto2() {
|
|
424
|
+
if (_crypto) return _crypto;
|
|
425
|
+
if (_crypto === null) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Webhook verification requires Node.js, Bun, or Deno (node:crypto not available)"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
_crypto = await import("crypto");
|
|
432
|
+
return _crypto;
|
|
433
|
+
} catch {
|
|
434
|
+
_crypto = null;
|
|
435
|
+
throw new Error(
|
|
436
|
+
"Webhook verification requires Node.js, Bun, or Deno (node:crypto not available)"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function verifyWebhookSignature(body, signature, secret) {
|
|
441
|
+
const crypto2 = await getNodeCrypto2();
|
|
442
|
+
const expected = crypto2.createHmac("sha256", secret).update(body).digest();
|
|
443
|
+
try {
|
|
444
|
+
return crypto2.timingSafeEqual(expected, Buffer.from(signature, "hex"));
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function parseWebhook(body, signature, secret) {
|
|
450
|
+
if (!await verifyWebhookSignature(body, signature, secret)) return null;
|
|
451
|
+
const text = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
452
|
+
return JSON.parse(text);
|
|
453
|
+
}
|
|
454
|
+
async function parseWebhookRequest(request, secret) {
|
|
455
|
+
const signature = request.headers.get("x-signature-256");
|
|
456
|
+
if (!signature) return null;
|
|
457
|
+
const body = await request.text();
|
|
458
|
+
return parseWebhook(body, signature, secret);
|
|
459
|
+
}
|
|
460
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
461
|
+
0 && (module.exports = {
|
|
462
|
+
RestGatewayClient,
|
|
463
|
+
detectDestinationType,
|
|
464
|
+
invoiceToDataUrl,
|
|
465
|
+
invoiceToSvg,
|
|
466
|
+
isBolt12Offer,
|
|
467
|
+
isLnAddress,
|
|
468
|
+
parseWebhook,
|
|
469
|
+
parseWebhookRequest,
|
|
470
|
+
solvePow,
|
|
471
|
+
verifyPow,
|
|
472
|
+
verifyWebhookSignature
|
|
473
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
type DonationStatus = "invoice_created" | "payment_received" | "paying_out" | "success" | "failed" | "expired";
|
|
2
|
+
/**
|
|
3
|
+
* Donation as returned by the gateway REST API.
|
|
4
|
+
*
|
|
5
|
+
* Field names use snake_case to match the JSON wire format. Pass server
|
|
6
|
+
* responses directly without mapping.
|
|
7
|
+
*/
|
|
8
|
+
interface Donation {
|
|
9
|
+
id: string;
|
|
10
|
+
ln_address: string;
|
|
11
|
+
amount_msat: number;
|
|
12
|
+
status: DonationStatus;
|
|
13
|
+
payment_hash: string;
|
|
14
|
+
bolt11: string;
|
|
15
|
+
payout_fee_msat: number | null;
|
|
16
|
+
error_message: string | null;
|
|
17
|
+
webhook_url: string | null;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
}
|
|
21
|
+
interface CreateDonationParams {
|
|
22
|
+
/**
|
|
23
|
+
* Payment destination: Lightning Address (`user@domain.com`)
|
|
24
|
+
* or BOLT12 offer (`lno1...`).
|
|
25
|
+
*/
|
|
26
|
+
destination: string;
|
|
27
|
+
/** Amount in satoshis. Minimum 1 sat. */
|
|
28
|
+
amountSat: number;
|
|
29
|
+
/**
|
|
30
|
+
* Optional webhook URL. Receives a POST with the donation payload
|
|
31
|
+
* immediately after successful payout. Retries on failure:
|
|
32
|
+
* 10s, 1m, 2m, 5m, 10m, 30m, 1h, then hourly up to 1 week.
|
|
33
|
+
*/
|
|
34
|
+
webhookUrl?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Optional HMAC-SHA256 secret. When set, the gateway signs webhook
|
|
37
|
+
* payloads and sends the hex signature in the `X-Signature-256` header.
|
|
38
|
+
*/
|
|
39
|
+
webhookSecret?: string;
|
|
40
|
+
}
|
|
41
|
+
interface CreateDonationResult {
|
|
42
|
+
id: string;
|
|
43
|
+
bolt11: string;
|
|
44
|
+
paymentHash: string;
|
|
45
|
+
}
|
|
46
|
+
interface FeeEstimate {
|
|
47
|
+
amount_sat: number;
|
|
48
|
+
receive_fee_sat: number;
|
|
49
|
+
send_fee_estimate_sat: number;
|
|
50
|
+
total_fee_estimate_sat: number;
|
|
51
|
+
payout_estimate_sat: number;
|
|
52
|
+
}
|
|
53
|
+
interface ListDonationsParams {
|
|
54
|
+
limit?: number;
|
|
55
|
+
offset?: number;
|
|
56
|
+
}
|
|
57
|
+
interface WaitForPaymentOptions {
|
|
58
|
+
/** Called on each status transition. */
|
|
59
|
+
onStatusChange?: (status: DonationStatus) => void;
|
|
60
|
+
/** Timeout in milliseconds. Rejects with error on expiry. */
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
}
|
|
63
|
+
interface PowChallenge {
|
|
64
|
+
challenge: string;
|
|
65
|
+
difficulty: number;
|
|
66
|
+
expires_in_secs: number;
|
|
67
|
+
}
|
|
68
|
+
/** Webhook POST body. See README for headers and retry schedule. */
|
|
69
|
+
interface WebhookPayload {
|
|
70
|
+
event: "donation.success";
|
|
71
|
+
donation: Donation;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Gateway REST + WebSocket client. Handles PoW automatically. */
|
|
75
|
+
declare class RestGatewayClient {
|
|
76
|
+
private baseUrl;
|
|
77
|
+
constructor(gatewayUrl: string);
|
|
78
|
+
/** Returns null when the gateway doesn't require PoW for this IP. */
|
|
79
|
+
getChallenge(): Promise<PowChallenge | null>;
|
|
80
|
+
/** Create a donation. Solves PoW challenge if the gateway requires one. */
|
|
81
|
+
createDonation(params: CreateDonationParams): Promise<CreateDonationResult>;
|
|
82
|
+
getDonation(id: string): Promise<Donation>;
|
|
83
|
+
listDonations(params?: ListDonationsParams): Promise<Donation[]>;
|
|
84
|
+
estimateFees(amountSat: number): Promise<FeeEstimate>;
|
|
85
|
+
/** Subscribe via WS, resolve on terminal status. */
|
|
86
|
+
waitForPayment(donationId: string, options?: WaitForPaymentOptions): Promise<DonationStatus>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hashcash PoW solver. Finds nonce where SHA256(challenge:nonce) has
|
|
91
|
+
* N leading zero bits. Auto-picks the fastest runtime strategy:
|
|
92
|
+
* node:crypto > pure JS SHA-256 > crypto.subtle (browser, async).
|
|
93
|
+
*/
|
|
94
|
+
declare function solvePow(challenge: string, difficulty: number): Promise<number>;
|
|
95
|
+
declare function verifyPow(challenge: string, nonce: number, difficulty: number): Promise<boolean>;
|
|
96
|
+
|
|
97
|
+
interface QrOptions {
|
|
98
|
+
/** SVG width/height in pixels. Default: 256. */
|
|
99
|
+
size?: number;
|
|
100
|
+
/** Dark module color. Default: "#000". */
|
|
101
|
+
color?: string;
|
|
102
|
+
/** Prepend `LIGHTNING:` prefix to BOLT11 for wallet compat. Default: true. */
|
|
103
|
+
withPrefix?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/** Generate an SVG QR code. Works with both BOLT11 invoices and BOLT12 offers. */
|
|
106
|
+
declare function invoiceToSvg(invoice: string, options?: QrOptions): string;
|
|
107
|
+
/** SVG data URL for use in `<img src="...">`. */
|
|
108
|
+
declare function invoiceToDataUrl(invoice: string, options?: QrOptions): string;
|
|
109
|
+
|
|
110
|
+
/** BOLT12 offer strings start with "lno1". */
|
|
111
|
+
declare function isBolt12Offer(s: string): boolean;
|
|
112
|
+
/** Basic format check (user@domain). Does not verify the domain. */
|
|
113
|
+
declare function isLnAddress(s: string): boolean;
|
|
114
|
+
type DestinationType = "bolt12Offer" | "lnAddress" | "unknown";
|
|
115
|
+
declare function detectDestinationType(s: string): DestinationType;
|
|
116
|
+
|
|
117
|
+
/** Verify `X-Signature-256` header. Pass the raw body, not parsed JSON. */
|
|
118
|
+
declare function verifyWebhookSignature(body: string | Buffer | Uint8Array, signature: string, secret: string): Promise<boolean>;
|
|
119
|
+
/** Verify + parse in one step. Returns null on bad signature. */
|
|
120
|
+
declare function parseWebhook(body: string | Buffer | Uint8Array, signature: string, secret: string): Promise<WebhookPayload | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Verify + parse from a Fetch API `Request` (Hono, Next.js, SvelteKit, etc.).
|
|
123
|
+
* Requires `node:crypto`. Won't work in browsers or Cloudflare Workers.
|
|
124
|
+
*/
|
|
125
|
+
declare function parseWebhookRequest(request: Request, secret: string): Promise<WebhookPayload | null>;
|
|
126
|
+
|
|
127
|
+
export { type CreateDonationParams, type CreateDonationResult, type DestinationType, type Donation, type DonationStatus, type FeeEstimate, type ListDonationsParams, type PowChallenge, type QrOptions, RestGatewayClient, type WaitForPaymentOptions, type WebhookPayload, detectDestinationType, invoiceToDataUrl, invoiceToSvg, isBolt12Offer, isLnAddress, parseWebhook, parseWebhookRequest, solvePow, verifyPow, verifyWebhookSignature };
|