@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.
@@ -1,55 +1,8 @@
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
- };
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 assertTokenAnswer(token, answer) {
257
- if (answer.paymentOption !== token.id) throw new LnurlError(`The callback answered for ${answer.paymentOption}, not ${token.id}`);
258
- if (!answer.paymentDestination || !isTokenAddress(token.asset.namespace, answer.paymentDestination)) {
259
- throw new LnurlError(`The callback returned no ${token.asset.namespace} address to pay`);
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
- const payment = answer.paymentQuote?.payment;
262
- const whole = typeof payment?.amount === "string" && /^[0-9]{1,78}$/.test(payment.amount) && /[1-9]/.test(payment.amount);
263
- if (!whole || typeof payment?.unit !== "string" || payment.unit.toUpperCase() !== token.unit.code) {
264
- throw new LnurlError(`The callback quoted no whole amount of ${token.unit.code}`);
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
- const unadvertised = opts.paymentOption !== void 0 && !payRequest.paymentOptions || opts.unit !== void 0 && !payRequest.units;
275
- if (payRequest.source.surface === "session" && unadvertised) {
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 amountMsat = opts.amountSat * 1e3;
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
- if (!Number.isSafeInteger(amountMsat)) {
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 (opts.unit !== void 0) params.set("unit", opts.unit);
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
- if (invoiceMsat !== amountMsat) {
309
- throw new LnurlError(`The callback returned an invoice for ${invoiceMsat ?? "no"} millisats, not the requested ${amountMsat}`);
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) assertTokenAnswer(token, result);
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
- const body = { arkadeAddress: req.arkadeAddress, claimPublicKey: req.claimPublicKey };
952
- if (req.boardingAddress !== void 0) body["boardingAddress"] = req.boardingAddress;
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 };