@arkade-os/lnurl-client 0.6.0 → 0.7.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 +141 -4
- package/dist/addresses-CPAzIbJ_.d.ts +514 -0
- package/dist/arkade.d.ts +81 -6
- package/dist/arkade.js +217 -16
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +60 -0
- package/dist/{chunk-YKBY6NCA.js → chunk-4ELYU6DY.js} +342 -82
- package/dist/chunk-H7W5RHP2.js +58 -0
- package/dist/hd.d.ts +16 -0
- package/dist/hd.js +389 -0
- package/dist/index.d.ts +81 -461
- package/dist/index.js +24 -4
- package/package.json +59 -43
|
@@ -1,55 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
httpStatus;
|
|
7
|
-
/** Machine-readable server code, when the management surface sent one. */
|
|
8
|
-
code;
|
|
9
|
-
/** True if and only if `httpStatus === 429`; every other failure is terminal. */
|
|
10
|
-
retryable;
|
|
11
|
-
/**
|
|
12
|
-
* Creates an `LnurlError`.
|
|
13
|
-
*
|
|
14
|
-
* @param reason - Human-readable failure reason, also used as the message.
|
|
15
|
-
* @param opts - Optional `httpStatus` and `code` captured from the response.
|
|
16
|
-
*/
|
|
17
|
-
constructor(reason, opts) {
|
|
18
|
-
super(reason);
|
|
19
|
-
this.name = "LnurlError";
|
|
20
|
-
this.reason = reason;
|
|
21
|
-
this.httpStatus = opts?.httpStatus;
|
|
22
|
-
this.code = opts?.code;
|
|
23
|
-
this.retryable = opts?.httpStatus === 429;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
var LnurlTransportError = class extends Error {
|
|
27
|
-
/**
|
|
28
|
-
* Creates an `LnurlTransportError`.
|
|
29
|
-
*
|
|
30
|
-
* @param message - Short description of which transport step failed.
|
|
31
|
-
* @param opts - Optional `cause` carrying the original thrown value.
|
|
32
|
-
*/
|
|
33
|
-
constructor(message, opts) {
|
|
34
|
-
super(message, opts);
|
|
35
|
-
this.name = "LnurlTransportError";
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
var LnurlTimeoutError = class extends Error {
|
|
39
|
-
/** The most recent verify snapshot seen before the deadline, when one was seen. */
|
|
40
|
-
lastSnapshot;
|
|
41
|
-
/**
|
|
42
|
-
* Creates an `LnurlTimeoutError`.
|
|
43
|
-
*
|
|
44
|
-
* @param message - Short description including the timeout that elapsed.
|
|
45
|
-
* @param opts - Optional `lastSnapshot` with the latest verify state.
|
|
46
|
-
*/
|
|
47
|
-
constructor(message, opts) {
|
|
48
|
-
super(message);
|
|
49
|
-
this.name = "LnurlTimeoutError";
|
|
50
|
-
this.lastSnapshot = opts?.lastSnapshot;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
LnurlError,
|
|
3
|
+
LnurlTimeoutError,
|
|
4
|
+
LnurlTransportError
|
|
5
|
+
} from "./chunk-H7W5RHP2.js";
|
|
53
6
|
|
|
54
7
|
// packages/client/src/http.ts
|
|
55
8
|
async function callFetch(url, init, fetchImpl) {
|
|
@@ -204,6 +157,87 @@ function tokenOptions(payRequest) {
|
|
|
204
157
|
});
|
|
205
158
|
}
|
|
206
159
|
|
|
160
|
+
// packages/client/src/units.ts
|
|
161
|
+
function formatUnitAmount(amount, decimals) {
|
|
162
|
+
const digits = String(amount).padStart(decimals + 1, "0");
|
|
163
|
+
return decimals ? `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}` : digits;
|
|
164
|
+
}
|
|
165
|
+
function parseUnitAmount(text, decimals) {
|
|
166
|
+
const match = /^(\d*)(?:\.(\d*))?$/.exec(text.trim());
|
|
167
|
+
if (!match || match[1] === "" && !match[2]) return void 0;
|
|
168
|
+
const fraction = match[2] ?? "";
|
|
169
|
+
if (fraction.length > decimals) return void 0;
|
|
170
|
+
const amount = Number(`${match[1]}${fraction.padEnd(decimals, "0")}`);
|
|
171
|
+
return Number.isSafeInteger(amount) && amount > 0 ? amount : void 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// packages/client/src/token-quotes.ts
|
|
175
|
+
var decimal = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
|
|
176
|
+
function tokenPayment(token, answer) {
|
|
177
|
+
if (answer.paymentOption !== token.id) throw new LnurlError(`The callback answered for ${answer.paymentOption}, not ${token.id}`);
|
|
178
|
+
const destination = answer.paymentDestination;
|
|
179
|
+
if (!destination || !isTokenAddress(token.asset.namespace, destination)) {
|
|
180
|
+
throw new LnurlError(`The callback returned no ${token.asset.namespace} address to pay`);
|
|
181
|
+
}
|
|
182
|
+
const payment = answer.paymentQuote?.payment;
|
|
183
|
+
const whole = typeof payment?.amount === "string" && /^[0-9]{1,78}$/.test(payment.amount) && /[1-9]/.test(payment.amount);
|
|
184
|
+
if (!whole || typeof payment?.unit !== "string" || payment.unit.toUpperCase() !== token.unit.code) {
|
|
185
|
+
throw new LnurlError(`The callback quoted no whole amount of ${token.unit.code}`);
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isInteger(token.unit.decimals) || token.unit.decimals < 0 || token.unit.decimals > 36) {
|
|
188
|
+
throw new LnurlError("The token unit has invalid decimals");
|
|
189
|
+
}
|
|
190
|
+
const baseUnits = payment.amount;
|
|
191
|
+
return {
|
|
192
|
+
destination,
|
|
193
|
+
baseUnits,
|
|
194
|
+
amount: decimal(formatUnitAmount(BigInt(baseUnits).toString(), token.unit.decimals)),
|
|
195
|
+
...answer.paymentDestinationTag !== void 0 ? { paymentDestinationTag: answer.paymentDestinationTag } : {}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function paysQuote(uri, { destination, baseUnits, amount, paymentDestinationTag }, asset) {
|
|
199
|
+
let url;
|
|
200
|
+
try {
|
|
201
|
+
url = new URL(uri);
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (url.hash) return false;
|
|
206
|
+
const q = url.searchParams;
|
|
207
|
+
const only = (key) => q.getAll(key).length === 1 ? q.get(key) : void 0;
|
|
208
|
+
if (asset.namespace === "eip155" && url.protocol === "ethereum:") {
|
|
209
|
+
const target = /^(0x[0-9a-f]{40})(?:@(\d+))?\/transfer$/i.exec(url.pathname);
|
|
210
|
+
let transferArgumentsOnly = true;
|
|
211
|
+
q.forEach((_value, key) => {
|
|
212
|
+
if (key !== "address" && key !== "uint256") transferArgumentsOnly = false;
|
|
213
|
+
});
|
|
214
|
+
return paymentDestinationTag === void 0 && target !== null && transferArgumentsOnly && target[1].toLowerCase() === asset.reference.toLowerCase() && (target[2] ?? "1") === asset.chainReference && only("address")?.toLowerCase() === destination.toLowerCase() && only("uint256") === baseUnits;
|
|
215
|
+
}
|
|
216
|
+
if (asset.namespace === "solana" && url.protocol === "solana:") {
|
|
217
|
+
const requestedAmount = only("amount");
|
|
218
|
+
return url.pathname === destination && only("spl-token") === asset.reference && q.getAll("memo").length <= 1 && (paymentDestinationTag === void 0 || only("memo") === paymentDestinationTag) && requestedAmount !== void 0 && /^\d+(?:\.\d+)?$/.test(requestedAmount) && decimal(requestedAmount) === amount;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function payableTokenUri(token, answer) {
|
|
223
|
+
const payment = tokenPayment(token, answer);
|
|
224
|
+
return answer.paymentURI && paysQuote(answer.paymentURI, payment, token.asset) ? answer.paymentURI : payment.destination;
|
|
225
|
+
}
|
|
226
|
+
function normalizeTokenQuote(token, answer, nowMs = Date.now()) {
|
|
227
|
+
const payment = tokenPayment(token, answer);
|
|
228
|
+
const rawExpiry = answer.paymentQuote?.expiresAt;
|
|
229
|
+
const expiresAt = typeof rawExpiry === "string" ? Date.parse(rawExpiry) : NaN;
|
|
230
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(expiresAt) || expiresAt <= nowMs) {
|
|
231
|
+
throw new LnurlError("The token quote has no valid future expiry");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
optionId: token.id,
|
|
235
|
+
...payment,
|
|
236
|
+
value: answer.paymentURI && paysQuote(answer.paymentURI, payment, token.asset) ? answer.paymentURI : payment.destination,
|
|
237
|
+
expiresAt
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
207
241
|
// packages/client/src/payer.ts
|
|
208
242
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
209
243
|
import { hex as hex2 } from "@scure/base";
|
|
@@ -253,16 +287,26 @@ function preimageOpens(preimage, pr) {
|
|
|
253
287
|
if (!hash || !/^[0-9a-f]{64}$/i.test(preimage)) return false;
|
|
254
288
|
return hex2.encode(sha2562(hex2.decode(preimage.toLowerCase()))) === hash;
|
|
255
289
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
if (!
|
|
259
|
-
|
|
290
|
+
function quotableUnit(payRequest, code, amount) {
|
|
291
|
+
const unit = payRequest.units?.find((u) => u.code.toUpperCase() === code.toUpperCase());
|
|
292
|
+
if (!unit) throw new LnurlError(`This payRequest does not quote in ${code}`);
|
|
293
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) throw new LnurlError(`Amount must be a positive whole number of ${unit.code}'s smallest unit`);
|
|
294
|
+
const bound = (b) => b !== void 0 && /^\d+$/.test(b) ? BigInt(b) : void 0;
|
|
295
|
+
const lo = bound(unit.minAmount);
|
|
296
|
+
const hi = bound(unit.maxAmount);
|
|
297
|
+
if (lo !== void 0 && BigInt(amount) < lo || hi !== void 0 && BigInt(amount) > hi) {
|
|
298
|
+
throw new LnurlError(`Amount must be between ${unit.minAmount ?? 1} and ${unit.maxAmount ?? "any"} in ${unit.code}'s smallest unit`);
|
|
260
299
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
300
|
+
return unit;
|
|
301
|
+
}
|
|
302
|
+
function quotedMsat(raw, unit, amount) {
|
|
303
|
+
const q = raw;
|
|
304
|
+
if (q?.requested?.unit !== unit || q.requested.amount !== String(amount)) {
|
|
305
|
+
throw new LnurlError(`The callback did not quote ${amount} ${unit}`);
|
|
265
306
|
}
|
|
307
|
+
const msat2 = q.payment?.unit === "msat" && /^\d+$/.test(q.payment.amount) ? Number(q.payment.amount) : NaN;
|
|
308
|
+
if (!Number.isSafeInteger(msat2) || msat2 <= 0) throw new LnurlError("The callback quoted no millisat amount to pay");
|
|
309
|
+
return msat2;
|
|
266
310
|
}
|
|
267
311
|
async function resolve(input, fetchImpl) {
|
|
268
312
|
const source = toPayRequestUrl(input);
|
|
@@ -271,20 +315,20 @@ async function resolve(input, fetchImpl) {
|
|
|
271
315
|
return { ...body, source };
|
|
272
316
|
}
|
|
273
317
|
async function requestInvoice(payRequest, opts, fetchImpl) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
throw new LnurlError("paymentOption and unit need a session payRequest that advertises them; this one does not");
|
|
318
|
+
if (payRequest.source.surface === "session" && opts.paymentOption !== void 0 && !payRequest.paymentOptions) {
|
|
319
|
+
throw new LnurlError("paymentOption needs a session payRequest that advertises options; this one does not");
|
|
277
320
|
}
|
|
278
|
-
const
|
|
321
|
+
const unit = opts.unit === void 0 ? void 0 : quotableUnit(payRequest, opts.unit, opts.amount);
|
|
279
322
|
const selected = opts.paymentOption ? payRequest.paymentOptions?.find((option) => option.id === opts.paymentOption) : void 0;
|
|
280
323
|
const token = selected?.asset !== void 0 ? tokenOptions(payRequest).find((o) => o.id === selected.id) : void 0;
|
|
281
324
|
if (selected?.asset !== void 0 && !token) throw new LnurlError(`paymentOption ${selected.id} is not a token this client can pay`);
|
|
282
325
|
const min = selected?.minSendable ?? payRequest.minSendable;
|
|
283
326
|
const max = selected?.maxSendable ?? payRequest.maxSendable;
|
|
284
|
-
|
|
327
|
+
const amountMsat = unit ? void 0 : opts.amountSat * 1e3;
|
|
328
|
+
if (amountMsat !== void 0 && !Number.isSafeInteger(amountMsat)) {
|
|
285
329
|
throw new LnurlError("Amount must be a whole number of millisats");
|
|
286
330
|
}
|
|
287
|
-
if (amountMsat < min || amountMsat > max) {
|
|
331
|
+
if (amountMsat !== void 0 && (amountMsat < min || amountMsat > max)) {
|
|
288
332
|
throw new LnurlError(`Amount must be between ${min} and ${max} millisats`);
|
|
289
333
|
}
|
|
290
334
|
if (opts.comment !== void 0 && opts.comment.length > 0) {
|
|
@@ -295,18 +339,19 @@ async function requestInvoice(payRequest, opts, fetchImpl) {
|
|
|
295
339
|
}
|
|
296
340
|
}
|
|
297
341
|
const params = new URLSearchParams();
|
|
298
|
-
params.set("amount", String(amountMsat));
|
|
342
|
+
params.set("amount", String(unit ? opts.amount : amountMsat));
|
|
299
343
|
if (opts.comment) params.set("comment", opts.comment);
|
|
300
344
|
if (opts.paymentOption !== void 0) params.set("paymentOption", opts.paymentOption);
|
|
301
|
-
if (
|
|
345
|
+
if (unit) params.set("unit", unit.code);
|
|
302
346
|
const sep = payRequest.callback.includes("?") ? "&" : "?";
|
|
303
347
|
const body = await lnurlFetch(`${payRequest.callback}${sep}${params.toString()}`, void 0, fetchImpl);
|
|
304
348
|
if (typeof body.pr === "string") {
|
|
305
349
|
if (token) throw new LnurlError(`The callback answered token option ${token.id} with an invoice`);
|
|
306
350
|
const invoiceMsat = amountMsatOf(body.pr);
|
|
307
351
|
if (invoiceMsat === void 0) throw new LnurlError("The callback returned an invoice that does not decode");
|
|
308
|
-
|
|
309
|
-
|
|
352
|
+
const expectedMsat = unit ? quotedMsat(body.paymentQuote, unit.code, opts.amount) : amountMsat;
|
|
353
|
+
if (invoiceMsat !== expectedMsat) {
|
|
354
|
+
throw new LnurlError(`The callback returned an invoice for ${invoiceMsat ?? "no"} millisats, not the ${unit ? "quoted" : "requested"} ${expectedMsat}`);
|
|
310
355
|
}
|
|
311
356
|
const result = { kind: "bolt11", pr: body.pr, verify: typeof body.verify === "string" ? body.verify : void 0 };
|
|
312
357
|
if (typeof body.verifyBatch === "string") result.verifyBatch = body.verifyBatch;
|
|
@@ -327,7 +372,11 @@ async function requestInvoice(payRequest, opts, fetchImpl) {
|
|
|
327
372
|
...typeof body.verify === "string" ? { verify: body.verify } : {},
|
|
328
373
|
...typeof body.verifyBatch === "string" ? { verifyBatch: body.verifyBatch } : {}
|
|
329
374
|
};
|
|
330
|
-
if (token)
|
|
375
|
+
if (token) {
|
|
376
|
+
const paymentURI = payableTokenUri(token, result);
|
|
377
|
+
if (result.paymentURI !== void 0) result.paymentURI = paymentURI;
|
|
378
|
+
}
|
|
379
|
+
if (unit) quotedMsat(body.paymentQuote, unit.code, opts.amount);
|
|
331
380
|
return result;
|
|
332
381
|
}
|
|
333
382
|
throw new LnurlError("Unexpected callback response");
|
|
@@ -898,7 +947,8 @@ function toActivity(row) {
|
|
|
898
947
|
settled: row.settled,
|
|
899
948
|
amountMsat: row.amountMsat,
|
|
900
949
|
createdAt: row.createdAt,
|
|
901
|
-
settledAt: row.settledAt
|
|
950
|
+
settledAt: row.settledAt,
|
|
951
|
+
...row.quote ? { quote: row.quote } : {}
|
|
902
952
|
};
|
|
903
953
|
return activity2;
|
|
904
954
|
}
|
|
@@ -913,7 +963,8 @@ function toActivity(row) {
|
|
|
913
963
|
settled: row.settled,
|
|
914
964
|
amountMsat: row.amountMsat,
|
|
915
965
|
createdAt: row.createdAt,
|
|
916
|
-
settledAt: row.settledAt
|
|
966
|
+
settledAt: row.settledAt,
|
|
967
|
+
...row.quote ? { quote: row.quote } : {}
|
|
917
968
|
};
|
|
918
969
|
return activity;
|
|
919
970
|
}
|
|
@@ -945,17 +996,47 @@ async function revokeAddress(baseUrl, token, handle, opts, fetchImpl) {
|
|
|
945
996
|
}
|
|
946
997
|
var COMPRESSED_KEY = /^0[23][0-9a-f]{64}$/i;
|
|
947
998
|
async function registerArkadeIdentity(baseUrl, req, fetchImpl) {
|
|
948
|
-
if (!COMPRESSED_KEY.test(req.claimPublicKey)) {
|
|
999
|
+
if (!req.hd && !COMPRESSED_KEY.test(req.claimPublicKey)) {
|
|
949
1000
|
throw new LnurlError("claimPublicKey must be a compressed 33-byte public key (02/03 prefix plus 64 hex chars)");
|
|
950
1001
|
}
|
|
951
|
-
|
|
952
|
-
|
|
1002
|
+
if (req.hd && (req.arkadeAddress !== void 0 || req.claimPublicKey !== void 0 || req.boardingAddress !== void 0)) {
|
|
1003
|
+
throw new LnurlError("HD receive cannot include a static wallet destination");
|
|
1004
|
+
}
|
|
1005
|
+
const body = req.hd ? { hd: req.hd } : { arkadeAddress: req.arkadeAddress, claimPublicKey: req.claimPublicKey };
|
|
1006
|
+
if (!req.hd && req.boardingAddress !== void 0) body["boardingAddress"] = req.boardingAddress;
|
|
953
1007
|
if (req.domain !== void 0) body["domain"] = req.domain;
|
|
954
|
-
await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(req.handle)}/arkade`, {
|
|
1008
|
+
const result = await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(req.handle)}/arkade`, {
|
|
955
1009
|
method: "POST",
|
|
956
1010
|
headers: { "content-type": "application/json", Authorization: `Bearer ${req.token}` },
|
|
957
1011
|
body: JSON.stringify(body)
|
|
958
1012
|
}, fetchImpl);
|
|
1013
|
+
if (req.hd && result?.receiveMode !== "hd") throw new LnurlError("Server did not confirm HD receive mode");
|
|
1014
|
+
}
|
|
1015
|
+
async function listHdAllocations(baseUrl, token, handle, opts, fetchImpl) {
|
|
1016
|
+
if (opts?.after !== void 0 && (!Number.isSafeInteger(opts.after) || opts.after < 0)) {
|
|
1017
|
+
throw new LnurlError("Allocation cursor must be a non-negative integer");
|
|
1018
|
+
}
|
|
1019
|
+
if (opts?.limit !== void 0 && (!Number.isSafeInteger(opts.limit) || opts.limit <= 0)) {
|
|
1020
|
+
throw new LnurlError("Allocation limit must be a positive integer");
|
|
1021
|
+
}
|
|
1022
|
+
const params = new URLSearchParams();
|
|
1023
|
+
if (opts?.domain !== void 0) params.set("domain", opts.domain);
|
|
1024
|
+
if (opts?.after !== void 0) params.set("after", String(opts.after));
|
|
1025
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
1026
|
+
const query = params.size ? `?${params.toString()}` : "";
|
|
1027
|
+
const page = await apiFetch(`${rootOf(baseUrl)}/lnurl/address/${encodeURIComponent(handle)}/arkade/allocations${query}`, {
|
|
1028
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1029
|
+
}, fetchImpl);
|
|
1030
|
+
if (!page || !Array.isArray(page.allocations) || !Number.isSafeInteger(page.nextAfter)) {
|
|
1031
|
+
throw new LnurlError("Invalid HD allocation page");
|
|
1032
|
+
}
|
|
1033
|
+
let previous = opts?.after ?? 0;
|
|
1034
|
+
for (const allocation of page.allocations) {
|
|
1035
|
+
if (!allocation || !Number.isSafeInteger(allocation.id) || allocation.id <= previous || !Number.isSafeInteger(allocation.index) || allocation.index < 0 || allocation.index >= 2147483648 || typeof allocation.descriptor !== "string" || typeof allocation.arkadeAddress !== "string" || !COMPRESSED_KEY.test(allocation.claimPublicKey) || !allocation.hd || allocation.hd.version !== 1 || !Number.isSafeInteger(allocation.createdAt)) throw new LnurlError("Invalid HD allocation page");
|
|
1036
|
+
previous = allocation.id;
|
|
1037
|
+
}
|
|
1038
|
+
if (page.nextAfter !== previous) throw new LnurlError("Invalid HD allocation cursor");
|
|
1039
|
+
return page;
|
|
959
1040
|
}
|
|
960
1041
|
|
|
961
1042
|
// packages/client/src/sync.ts
|
|
@@ -988,7 +1069,8 @@ var toStored = (baseUrl, domain, lightningAddress, handle, entry) => {
|
|
|
988
1069
|
payoutReference: entry.payoutReference ?? null,
|
|
989
1070
|
preimage: entry.kind === "bolt11" ? entry.preimage : null,
|
|
990
1071
|
paymentOption: entry.kind === "destination" ? entry.paymentOption : null,
|
|
991
|
-
covenantScript: entry.kind === "destination" ? entry.covenantScript : null
|
|
1072
|
+
covenantScript: entry.kind === "destination" ? entry.covenantScript : null,
|
|
1073
|
+
...entry.quote ? { quote: entry.quote } : {}
|
|
992
1074
|
};
|
|
993
1075
|
};
|
|
994
1076
|
var mayStillSettle = (row) => !row.settled && (row.paymentOption === null || !NEVER_SETTLES.has(row.paymentOption));
|
|
@@ -1097,6 +1179,177 @@ function forgetStoredPayments(prefix = "") {
|
|
|
1097
1179
|
area().removeItem(`${prefix}${WATERMARKS}`);
|
|
1098
1180
|
}
|
|
1099
1181
|
|
|
1182
|
+
// packages/client/src/confirmations.ts
|
|
1183
|
+
var POLL_MS = 2e3;
|
|
1184
|
+
var BATCH_TIMEOUT_MS = 1e4;
|
|
1185
|
+
var CONFIRMATION_TIMEOUT_MS = 18e4;
|
|
1186
|
+
function createPendingConfirmations(client = createLnurlClient()) {
|
|
1187
|
+
const entries = /* @__PURE__ */ new Set();
|
|
1188
|
+
const batches = /* @__PURE__ */ new Set();
|
|
1189
|
+
let timer;
|
|
1190
|
+
let ticking = false;
|
|
1191
|
+
let generation = 0;
|
|
1192
|
+
function maybeStop() {
|
|
1193
|
+
if (timer && entries.size === 0) {
|
|
1194
|
+
clearInterval(timer);
|
|
1195
|
+
timer = void 0;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function finish(entry, notify) {
|
|
1199
|
+
if (!entries.delete(entry)) return;
|
|
1200
|
+
entry.soloPolling?.abort();
|
|
1201
|
+
maybeStop();
|
|
1202
|
+
try {
|
|
1203
|
+
notify();
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const fail = (entry, err) => finish(entry, () => entry.onError(err));
|
|
1208
|
+
const expire = (entry) => {
|
|
1209
|
+
if (Date.now() < entry.deadline) return false;
|
|
1210
|
+
fail(entry, new Error("receiver did not confirm settlement in time"));
|
|
1211
|
+
return true;
|
|
1212
|
+
};
|
|
1213
|
+
function settle(entry, status) {
|
|
1214
|
+
if (expire(entry)) return;
|
|
1215
|
+
if (entry.expect && !verifyStatusIsFor(status, entry.expect)) {
|
|
1216
|
+
fail(entry, new Error("verify answered about a different payment"));
|
|
1217
|
+
} else if (status.settled) {
|
|
1218
|
+
finish(entry, () => entry.onSettled(status));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function tick(current) {
|
|
1222
|
+
const now = Date.now();
|
|
1223
|
+
for (const entry of [...entries]) expire(entry);
|
|
1224
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1225
|
+
for (const entry of entries) {
|
|
1226
|
+
if (entry.soloPolling) continue;
|
|
1227
|
+
if (entry.verifyBatch === void 0) {
|
|
1228
|
+
entry.soloPolling = new AbortController();
|
|
1229
|
+
void Promise.resolve().then(() => client.pollVerify(entry.verifyUrl, {
|
|
1230
|
+
intervalMs: POLL_MS,
|
|
1231
|
+
timeoutMs: entry.deadline - now,
|
|
1232
|
+
signal: entry.soloPolling.signal,
|
|
1233
|
+
...entry.expect ? { expect: entry.expect } : {}
|
|
1234
|
+
})).then((status) => settle(entry, status)).catch((err) => fail(entry, err instanceof Error ? err : new Error(String(err))));
|
|
1235
|
+
} else {
|
|
1236
|
+
const group = groups.get(entry.verifyBatch) ?? [];
|
|
1237
|
+
group.push(entry);
|
|
1238
|
+
groups.set(entry.verifyBatch, group);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
for (const [endpoint, group] of groups) {
|
|
1242
|
+
if (current !== generation) return;
|
|
1243
|
+
const active = group.filter((entry) => entries.has(entry) && !expire(entry));
|
|
1244
|
+
if (!active.length) continue;
|
|
1245
|
+
const abort = new AbortController();
|
|
1246
|
+
batches.add(abort);
|
|
1247
|
+
const cancelled = new Promise((_, reject) => {
|
|
1248
|
+
abort.signal.addEventListener("abort", () => reject(new Error("batch verify aborted")), { once: true });
|
|
1249
|
+
});
|
|
1250
|
+
const cutoff = setTimeout(() => abort.abort(), Math.max(
|
|
1251
|
+
0,
|
|
1252
|
+
Math.min(BATCH_TIMEOUT_MS, ...active.map((entry) => entry.deadline - Date.now()))
|
|
1253
|
+
));
|
|
1254
|
+
try {
|
|
1255
|
+
const body = await Promise.race([
|
|
1256
|
+
client.batchVerify(endpoint, [...new Set(active.map((entry) => entry.verifyUrl))], { signal: abort.signal }),
|
|
1257
|
+
cancelled
|
|
1258
|
+
]);
|
|
1259
|
+
for (const entry of active) {
|
|
1260
|
+
const answer = body.results[entry.verifyUrl];
|
|
1261
|
+
if (!answer || answer.kind !== "verify") {
|
|
1262
|
+
fail(entry, new Error(answer?.reason ?? "no answer for this verify URL"));
|
|
1263
|
+
} else {
|
|
1264
|
+
settle(entry, answer.status);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
} catch {
|
|
1268
|
+
} finally {
|
|
1269
|
+
clearTimeout(cutoff);
|
|
1270
|
+
batches.delete(abort);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function ensure() {
|
|
1275
|
+
if (timer) return;
|
|
1276
|
+
timer = setInterval(() => {
|
|
1277
|
+
if (ticking) return;
|
|
1278
|
+
const current = generation;
|
|
1279
|
+
ticking = true;
|
|
1280
|
+
void tick(current).finally(() => {
|
|
1281
|
+
if (current === generation) ticking = false;
|
|
1282
|
+
});
|
|
1283
|
+
}, POLL_MS);
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
add(confirmation) {
|
|
1287
|
+
if ([...entries].some((entry) => (entry.key ?? entry.verifyUrl) === (confirmation.key ?? confirmation.verifyUrl))) return;
|
|
1288
|
+
entries.add({
|
|
1289
|
+
...confirmation,
|
|
1290
|
+
deadline: (confirmation.startedAt ?? Date.now()) + (confirmation.timeoutMs ?? CONFIRMATION_TIMEOUT_MS)
|
|
1291
|
+
});
|
|
1292
|
+
ensure();
|
|
1293
|
+
},
|
|
1294
|
+
forget() {
|
|
1295
|
+
const forgotten = [...entries];
|
|
1296
|
+
entries.clear();
|
|
1297
|
+
generation++;
|
|
1298
|
+
ticking = false;
|
|
1299
|
+
maybeStop();
|
|
1300
|
+
for (const entry of forgotten) entry.soloPolling?.abort();
|
|
1301
|
+
for (const abort of batches) abort.abort();
|
|
1302
|
+
batches.clear();
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// packages/client/src/repository.ts
|
|
1308
|
+
function createLnurlPaymentRepository(payments, watermarks) {
|
|
1309
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1310
|
+
let closed = false;
|
|
1311
|
+
const write = async (work) => {
|
|
1312
|
+
if (closed) return;
|
|
1313
|
+
const pending = Promise.resolve().then(work);
|
|
1314
|
+
inFlight.add(pending);
|
|
1315
|
+
try {
|
|
1316
|
+
await pending;
|
|
1317
|
+
} finally {
|
|
1318
|
+
inFlight.delete(pending);
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
return {
|
|
1322
|
+
all: () => payments.read(),
|
|
1323
|
+
upsert: (records) => write(() => payments.write(records)),
|
|
1324
|
+
readWatermark: (baseUrl, address) => watermarks.read(baseUrl, address),
|
|
1325
|
+
writeWatermark: (baseUrl, address, since) => write(() => watermarks.write(baseUrl, address, since)),
|
|
1326
|
+
clear: async () => {
|
|
1327
|
+
await watermarks.clear();
|
|
1328
|
+
await payments.clear();
|
|
1329
|
+
},
|
|
1330
|
+
closeWrites: () => {
|
|
1331
|
+
closed = true;
|
|
1332
|
+
},
|
|
1333
|
+
reopenWrites: () => {
|
|
1334
|
+
closed = false;
|
|
1335
|
+
},
|
|
1336
|
+
writesClosed: () => closed,
|
|
1337
|
+
writesSettled: async () => {
|
|
1338
|
+
await Promise.allSettled([...inFlight]);
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
async function clearLnurlPaymentsForReset(repository) {
|
|
1343
|
+
repository.closeWrites();
|
|
1344
|
+
try {
|
|
1345
|
+
await repository.writesSettled();
|
|
1346
|
+
await repository.clear();
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
repository.reopenWrites();
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1100
1353
|
// packages/client/src/index.ts
|
|
1101
1354
|
function createLnurlClient(opts) {
|
|
1102
1355
|
const baseUrl = opts?.baseUrl;
|
|
@@ -1109,7 +1362,7 @@ function createLnurlClient(opts) {
|
|
|
1109
1362
|
resolve: (input) => resolve(input, fetchImpl),
|
|
1110
1363
|
requestInvoice: (payRequest, reqOpts) => requestInvoice(payRequest, reqOpts, fetchImpl),
|
|
1111
1364
|
pollVerify: (verifyUrl, pollOpts) => pollVerify(verifyUrl, pollOpts, fetchImpl),
|
|
1112
|
-
batchVerify: (verifyBatchUrl, verifyUrls, opts2) => batchVerify(verifyBatchUrl, verifyUrls, opts2, opts2?.fetchImpl),
|
|
1365
|
+
batchVerify: (verifyBatchUrl, verifyUrls, opts2) => batchVerify(verifyBatchUrl, verifyUrls, opts2, opts2?.fetchImpl ?? fetchImpl),
|
|
1113
1366
|
openSession: async (sessionOpts, handlers) => openSession(needBase("openSession"), sessionOpts, handlers, fetchImpl),
|
|
1114
1367
|
openVerifyBatchStream: (streamOpts, handlers) => openVerifyBatchStream(streamOpts, handlers, streamOpts?.fetchImpl),
|
|
1115
1368
|
registerAddress: async (req) => registerAddress(needBase("registerAddress"), req, fetchImpl),
|
|
@@ -1118,14 +1371,12 @@ function createLnurlClient(opts) {
|
|
|
1118
1371
|
listAddresses: async (token) => listAddresses(needBase("listAddresses"), token, fetchImpl),
|
|
1119
1372
|
revokeAddress: async (token, handle, revokeOpts) => revokeAddress(needBase("revokeAddress"), token, handle, revokeOpts, fetchImpl),
|
|
1120
1373
|
registerArkadeIdentity: async (req) => registerArkadeIdentity(needBase("registerArkadeIdentity"), req, fetchImpl),
|
|
1374
|
+
listHdAllocations: async (token, handle, listOpts) => listHdAllocations(needBase("listHdAllocations"), token, handle, listOpts, fetchImpl),
|
|
1121
1375
|
listPayments: async (token, handle, listOpts) => listPayments(needBase("listPayments"), token, handle, listOpts, fetchImpl)
|
|
1122
1376
|
};
|
|
1123
1377
|
}
|
|
1124
1378
|
|
|
1125
1379
|
export {
|
|
1126
|
-
LnurlError,
|
|
1127
|
-
LnurlTransportError,
|
|
1128
|
-
LnurlTimeoutError,
|
|
1129
1380
|
deriveSessionToken,
|
|
1130
1381
|
deriveSessionTokenWithSigner,
|
|
1131
1382
|
deriveSessionId,
|
|
@@ -1135,6 +1386,10 @@ export {
|
|
|
1135
1386
|
toPayRequestUrl,
|
|
1136
1387
|
isTokenAddress,
|
|
1137
1388
|
tokenOptions,
|
|
1389
|
+
formatUnitAmount,
|
|
1390
|
+
parseUnitAmount,
|
|
1391
|
+
payableTokenUri,
|
|
1392
|
+
normalizeTokenQuote,
|
|
1138
1393
|
resolve,
|
|
1139
1394
|
requestInvoice,
|
|
1140
1395
|
verifyStatusIsFor,
|
|
@@ -1149,9 +1404,14 @@ export {
|
|
|
1149
1404
|
listPayments,
|
|
1150
1405
|
revokeAddress,
|
|
1151
1406
|
registerArkadeIdentity,
|
|
1407
|
+
listHdAllocations,
|
|
1152
1408
|
syncPayments,
|
|
1153
1409
|
browserPaymentStore,
|
|
1154
1410
|
storedPayments,
|
|
1155
1411
|
forgetStoredPayments,
|
|
1412
|
+
CONFIRMATION_TIMEOUT_MS,
|
|
1413
|
+
createPendingConfirmations,
|
|
1414
|
+
createLnurlPaymentRepository,
|
|
1415
|
+
clearLnurlPaymentsForReset,
|
|
1156
1416
|
createLnurlClient
|
|
1157
1417
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// packages/client/src/errors.ts
|
|
2
|
+
var LnurlError = class extends Error {
|
|
3
|
+
/** Human-readable failure reason, taken from the wire `reason`/`error` field. */
|
|
4
|
+
reason;
|
|
5
|
+
/** HTTP status when the failure came with one; absent for LUD-06 bodies answered at HTTP 200. */
|
|
6
|
+
httpStatus;
|
|
7
|
+
/** Machine-readable server code, when the management surface sent one. */
|
|
8
|
+
code;
|
|
9
|
+
/** True if and only if `httpStatus === 429`; every other failure is terminal. */
|
|
10
|
+
retryable;
|
|
11
|
+
/**
|
|
12
|
+
* Creates an `LnurlError`.
|
|
13
|
+
*
|
|
14
|
+
* @param reason - Human-readable failure reason, also used as the message.
|
|
15
|
+
* @param opts - Optional `httpStatus` and `code` captured from the response.
|
|
16
|
+
*/
|
|
17
|
+
constructor(reason, opts) {
|
|
18
|
+
super(reason);
|
|
19
|
+
this.name = "LnurlError";
|
|
20
|
+
this.reason = reason;
|
|
21
|
+
this.httpStatus = opts?.httpStatus;
|
|
22
|
+
this.code = opts?.code;
|
|
23
|
+
this.retryable = opts?.httpStatus === 429;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var LnurlTransportError = class extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* Creates an `LnurlTransportError`.
|
|
29
|
+
*
|
|
30
|
+
* @param message - Short description of which transport step failed.
|
|
31
|
+
* @param opts - Optional `cause` carrying the original thrown value.
|
|
32
|
+
*/
|
|
33
|
+
constructor(message, opts) {
|
|
34
|
+
super(message, opts);
|
|
35
|
+
this.name = "LnurlTransportError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var LnurlTimeoutError = class extends Error {
|
|
39
|
+
/** The most recent verify snapshot seen before the deadline, when one was seen. */
|
|
40
|
+
lastSnapshot;
|
|
41
|
+
/**
|
|
42
|
+
* Creates an `LnurlTimeoutError`.
|
|
43
|
+
*
|
|
44
|
+
* @param message - Short description including the timeout that elapsed.
|
|
45
|
+
* @param opts - Optional `lastSnapshot` with the latest verify state.
|
|
46
|
+
*/
|
|
47
|
+
constructor(message, opts) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "LnurlTimeoutError";
|
|
50
|
+
this.lastSnapshot = opts?.lastSnapshot;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
LnurlError,
|
|
56
|
+
LnurlTransportError,
|
|
57
|
+
LnurlTimeoutError
|
|
58
|
+
};
|
package/dist/hd.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as _arkade_os_sdk from '@arkade-os/sdk';
|
|
2
|
+
import { IContractManager } from '@arkade-os/sdk';
|
|
3
|
+
import { b as HdReceiveConfig, c as HdReceiveAllocation } from './addresses-CPAzIbJ_.js';
|
|
4
|
+
export { H as HdReceiveAllocationPage } from './addresses-CPAzIbJ_.js';
|
|
5
|
+
|
|
6
|
+
interface HdReceiveBranch {
|
|
7
|
+
hd: HdReceiveConfig;
|
|
8
|
+
signingDescriptor(index: number): string;
|
|
9
|
+
}
|
|
10
|
+
declare function createHdReceiveBranch(params: Omit<HdReceiveConfig, "version" | "descriptor"> & {
|
|
11
|
+
accountDescriptor: string;
|
|
12
|
+
branchPath: string;
|
|
13
|
+
}): HdReceiveBranch;
|
|
14
|
+
declare function restoreHdReceiveAllocation(manager: Pick<IContractManager, "createContract">, allocation: HdReceiveAllocation, branch: HdReceiveBranch): Promise<_arkade_os_sdk.Contract>;
|
|
15
|
+
|
|
16
|
+
export { HdReceiveAllocation, type HdReceiveBranch, HdReceiveConfig, createHdReceiveBranch, restoreHdReceiveAllocation };
|