@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/dist/arkade.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import {
2
- LnurlError,
3
2
  createLnurlClient,
3
+ createPendingConfirmations,
4
4
  deriveSessionId,
5
5
  deriveSessionTokenWithSigner,
6
+ formatUnitAmount,
6
7
  syncPayments
7
- } from "./chunk-YKBY6NCA.js";
8
+ } from "./chunk-4ELYU6DY.js";
9
+ import {
10
+ LnurlError
11
+ } from "./chunk-H7W5RHP2.js";
8
12
 
9
13
  // packages/client/src/arkade.ts
10
- import { ArkAddress } from "@arkade-os/sdk";
11
- import { hex } from "@scure/base";
14
+ import { ArkAddress as ArkAddress2 } from "@arkade-os/sdk";
15
+ import { hex as hex2 } from "@scure/base";
12
16
 
13
17
  // packages/client/src/rail.ts
14
18
  import { isLnurl } from "@arkade-os/sdk";
@@ -54,6 +58,11 @@ function makeRail(type, id, inner, deps, resolveOnce) {
54
58
  } catch {
55
59
  return false;
56
60
  }
61
+ if (req.unitAmount) {
62
+ if (!unitOf(pr, req.unitAmount.unit)) return false;
63
+ if (pr.paymentOptions === void 0) return type === "lightning";
64
+ return pr.paymentOptions.some((o) => o.type === type && o.available !== false);
65
+ }
57
66
  if (pr.paymentOptions === void 0) {
58
67
  if (type !== "lightning") return false;
59
68
  if (req.amount === void 0) return true;
@@ -68,24 +77,41 @@ function makeRail(type, id, inner, deps, resolveOnce) {
68
77
  },
69
78
  async quote(req, ctx) {
70
79
  const pr = await resolveOnce(req.raw);
71
- if (req.amount === void 0) throw new LnurlError(`${id} needs an explicit amount`);
80
+ const inUnit = req.unitAmount;
81
+ if (req.amount === void 0 && !inUnit) throw new LnurlError(`${id} needs an explicit amount`);
72
82
  const option = pr.paymentOptions?.find((o) => o.type === type && o.available !== false)?.id;
73
- const result = await deps.client.requestInvoice(pr, {
74
- amountSat: req.amount,
83
+ const extra = {
75
84
  ...option !== void 0 ? { paymentOption: option } : {},
76
85
  ...deps.comment !== void 0 ? { comment: deps.comment } : {}
77
- });
78
- const delegated = result.kind === "bolt11" ? { raw: result.pr } : { raw: destinationOf(result.paymentDestination, id), amount: req.amount };
86
+ };
87
+ const result = await deps.client.requestInvoice(
88
+ pr,
89
+ inUnit ? { ...extra, unit: inUnit.unit, amount: inUnit.amount } : { ...extra, amountSat: req.amount }
90
+ );
91
+ const quotedSat = inUnit ? Number(result.paymentQuote?.payment.amount) / 1e3 : req.amount;
92
+ if (!Number.isSafeInteger(quotedSat)) throw new LnurlError(`${id}: the quote names no whole number of sats`);
93
+ const delegated = result.kind === "bolt11" ? { raw: result.pr } : { raw: destinationOf(result.paymentDestination, id), amount: quotedSat };
79
94
  if (!inner.match(delegated, ctx)) {
80
95
  throw new LnurlError(`${id}: ${inner.id} cannot pay the destination the callback returned`);
81
96
  }
82
97
  const quote = await inner.quote(delegated, ctx);
83
98
  const expect = result.kind === "bolt11" ? { pr: result.pr } : { paymentOption: result.paymentOption, paymentDestination: result.paymentDestination };
84
- const lnurl = { target: req.raw, via: inner.id, verify: result.verify, verifyBatch: result.verifyBatch, expect };
99
+ const unit = inUnit ? unitOf(pr, inUnit.unit) : void 0;
100
+ const lnurl = {
101
+ target: req.raw,
102
+ via: inner.id,
103
+ verify: result.verify,
104
+ verifyBatch: result.verifyBatch,
105
+ expect,
106
+ ...inUnit && unit ? { quote: { amount: String(inUnit.amount), unit: unit.code, decimals: unit.decimals } } : {}
107
+ };
85
108
  return { ...quote, railId: id, meta: { ...quote.meta, lnurl } };
86
109
  }
87
110
  };
88
111
  }
112
+ function unitOf(pr, code) {
113
+ return pr.units?.find((u) => u.code.toUpperCase() === code.toUpperCase());
114
+ }
89
115
  function destinationOf(destination, id) {
90
116
  if (!destination) throw new LnurlError(`${id}: the callback returned no payment destination`);
91
117
  return destination;
@@ -139,7 +165,11 @@ function arkadeLnurl(opts) {
139
165
  const { wallet, baseUrl } = opts;
140
166
  const domain = opts.domain ?? new URL(baseUrl).hostname;
141
167
  const identity = wallet ? wallet.identity : opts.identity;
142
- const addresses = async () => wallet ? { arkadeAddress: await wallet.getAddress(), boardingAddress: await wallet.getBoardingAddress() } : { arkadeAddress: opts.arkadeAddress, ...opts.boardingAddress ? { boardingAddress: opts.boardingAddress } : {} };
168
+ const addresses = async () => {
169
+ if (wallet) return { arkadeAddress: await wallet.getAddress(), boardingAddress: await wallet.getBoardingAddress() };
170
+ if (!opts.arkadeAddress) throw new LnurlError("Static receive requires an Arkade address");
171
+ return { arkadeAddress: opts.arkadeAddress, ...opts.boardingAddress ? { boardingAddress: opts.boardingAddress } : {} };
172
+ };
143
173
  const client = opts.client ?? createLnurlClient({ baseUrl });
144
174
  const payer = createLnurlClient();
145
175
  let tokenOnce;
@@ -180,6 +210,7 @@ function arkadeLnurl(opts) {
180
210
  return entry.pr;
181
211
  },
182
212
  payments: (listOpts) => client.listPayments(held, handle, { domain, ...listOpts }),
213
+ allocations: (listOpts) => client.listHdAllocations(held, handle, { domain, ...listOpts }),
183
214
  sync: async () => syncPayments(
184
215
  [{ baseUrl, token: held, handle, domain }],
185
216
  { client: (target) => createLnurlClient({ baseUrl: target }), store: needStore() }
@@ -216,7 +247,7 @@ function arkadeLnurl(opts) {
216
247
  );
217
248
  }
218
249
  await client.registerArkadeIdentity(
219
- await arkadeIdentityRequest({
250
+ opts.hd ? { token: held, handle: registered.handle, domain, hd: opts.hd } : await arkadeIdentityRequest({
220
251
  identity,
221
252
  token: held,
222
253
  handle: registered.handle,
@@ -227,11 +258,15 @@ function arkadeLnurl(opts) {
227
258
  return receiverFrom(held, registered.handle, registered.lightningAddress, flagged);
228
259
  },
229
260
  router,
230
- options: (target, amountSat, prefs) => router().options({ raw: target.trim(), amount: amountSat }, { priority: DEFAULT_RAIL_PRIORITY, ...prefs })
261
+ options: (target, amount, prefs) => router().options(
262
+ typeof amount === "number" ? { raw: target.trim(), amount } : { raw: target.trim(), unitAmount: amount },
263
+ { priority: DEFAULT_RAIL_PRIORITY, ...prefs }
264
+ )
231
265
  };
232
266
  }
233
267
 
234
268
  // packages/client/src/activity.ts
269
+ var quotedLabel = (q) => `${formatUnitAmount(q.amount, q.decimals)} ${q.unit}`;
235
270
  var LNURL_RESOLVER_ID = "lnurl:payments";
236
271
  var SENT_RESOLVER_ID = "lnurl:sends";
237
272
  var LNURL_GROUP_PREFIX = "lnurl:";
@@ -264,6 +299,7 @@ function sentActivityResolver(sends) {
264
299
  target: sent.target,
265
300
  rail: sent.railId,
266
301
  delivered: `${sent.amountSat} sats`,
302
+ ...sent.quote ? { requested: quotedLabel(sent.quote) } : {},
267
303
  ...sent.feeSat > 0 ? { fee: `${sent.feeSat} sats` } : {},
268
304
  ...sent.swapId ? { swap: sent.swapId } : {},
269
305
  ...sent.preimage ? { preimage: sent.preimage } : {},
@@ -293,17 +329,176 @@ function lnurlActivityResolver(payments) {
293
329
  rail: railOf(payment),
294
330
  lightningAddress: payment.lightningAddress,
295
331
  identifier: payment.identifier,
296
- verified: payment.settled
332
+ verified: payment.settled,
333
+ ...payment.quote ? { requested: quotedLabel(payment.quote) } : {}
297
334
  }
298
335
  }];
299
336
  }
300
337
  };
301
338
  }
302
339
 
340
+ // packages/client/src/sends.ts
341
+ function createLnurlSendTracker(store, options) {
342
+ const confirmations = options.confirmations ?? createPendingConfirmations();
343
+ const subscriptions = /* @__PURE__ */ new Set();
344
+ let generation = 0;
345
+ const persist = (update) => {
346
+ try {
347
+ const rows = update(store.read());
348
+ if (rows) store.write(rows);
349
+ } catch (error) {
350
+ options.onError(error);
351
+ }
352
+ };
353
+ const save = (sent) => persist((rows) => mergeSentPayment(rows, sent));
354
+ const mark = (txid, confirmed) => persist((rows) => {
355
+ const sent = rows.find((row) => row.txid === txid);
356
+ return sent ? mergeSentPayment(rows, { ...sent, receiverConfirmed: confirmed }) : void 0;
357
+ });
358
+ const watch = (txid, verify, verifyBatch, startedAt, expect) => {
359
+ const current = generation;
360
+ confirmations.add({
361
+ key: txid,
362
+ verifyUrl: verify,
363
+ verifyBatch,
364
+ startedAt,
365
+ expect,
366
+ onSettled: () => {
367
+ if (current === generation) mark(txid, true);
368
+ },
369
+ onError: () => {
370
+ if (current === generation) mark(txid, false);
371
+ }
372
+ });
373
+ };
374
+ return {
375
+ all: () => store.read(),
376
+ record(handle, quote, target) {
377
+ const createdAt = Date.now();
378
+ const current = generation;
379
+ const meta = lnurlQuoteMeta(quote);
380
+ let stopped = false;
381
+ let unsubscribe;
382
+ const stop = () => {
383
+ stopped = true;
384
+ unsubscribe?.();
385
+ subscriptions.delete(stop);
386
+ };
387
+ subscriptions.add(stop);
388
+ unsubscribe = handle.subscribe(({ status, result }) => {
389
+ if (stopped || current !== generation) return;
390
+ try {
391
+ if (result?.txid) save({
392
+ txid: result.txid,
393
+ target: meta?.target ?? target,
394
+ railId: quote.railId,
395
+ amountSat: quote.amount,
396
+ feeSat: quote.fee,
397
+ createdAt,
398
+ ...result.swapId ? { swapId: result.swapId } : {},
399
+ ...result.preimage ? { preimage: result.preimage } : {},
400
+ ...meta?.verify ? { verify: meta.verify } : {},
401
+ ...meta?.verifyBatch ? { verifyBatch: meta.verifyBatch } : {},
402
+ ...meta?.expect ? { expect: meta.expect } : {},
403
+ ...meta?.quote ? { quote: meta.quote } : {}
404
+ });
405
+ } finally {
406
+ if (status === "settled" || status === "failed") stop();
407
+ }
408
+ });
409
+ if (stopped) unsubscribe();
410
+ return stop;
411
+ },
412
+ watch,
413
+ resume() {
414
+ for (const sent of store.read()) {
415
+ if (sent.verify && sent.receiverConfirmed === void 0)
416
+ watch(sent.txid, sent.verify, sent.verifyBatch, sent.createdAt, sent.expect);
417
+ }
418
+ },
419
+ mark,
420
+ forget() {
421
+ generation++;
422
+ confirmations.forget();
423
+ for (const stop of subscriptions) stop();
424
+ }
425
+ };
426
+ }
427
+
428
+ // packages/client/src/receive-rails.ts
429
+ import { ArkAddress } from "@arkade-os/sdk";
430
+ import { hex } from "@scure/base";
431
+ function lnurlRailLimits(payRequest) {
432
+ const options = payRequest.paymentOptions ?? [{ id: "lightning", type: "lightning" }];
433
+ return options.filter((option) => option.id !== "" && option.available !== false && option.asset === void 0).map((option) => ({
434
+ type: option.type,
435
+ min: Math.ceil((option.minSendable ?? payRequest.minSendable) / 1e3),
436
+ max: Math.floor((option.maxSendable ?? payRequest.maxSendable) / 1e3)
437
+ }));
438
+ }
439
+ function sameOperator(a, b) {
440
+ try {
441
+ const [x, y] = [ArkAddress.decode(a), ArkAddress.decode(b)];
442
+ return x.hrp === y.hrp && hex.encode(x.serverPubKey) === hex.encode(y.serverPubKey);
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+ async function resolveLnurlRails(receiver, amountSat, rails, own, client = createLnurlClient()) {
448
+ const payRequest = await receiver.payRequest();
449
+ return Promise.allSettled(rails.map(async (rail) => {
450
+ const option = payRequest.paymentOptions?.find((candidate) => candidate.id !== "" && candidate.type === rail && candidate.available !== false && candidate.asset === void 0);
451
+ const legacyLightning = payRequest.paymentOptions === void 0 && rail === "lightning";
452
+ if (!option && !legacyLightning) throw new LnurlError(`the LNURL offers no ${rail} rail`);
453
+ const min = option?.minSendable ?? payRequest.minSendable;
454
+ const max = option?.maxSendable ?? payRequest.maxSendable;
455
+ const amountMsat = amountSat * 1e3;
456
+ if (!Number.isSafeInteger(amountMsat) || amountMsat < min || amountMsat > max) {
457
+ throw new LnurlError(`Amount must be between ${min} and ${max} whole millisats`);
458
+ }
459
+ const result = await client.requestInvoice(payRequest, { amountSat, paymentOption: option?.id });
460
+ if (option && result.paymentOption !== void 0 && result.paymentOption !== option.id) {
461
+ throw new LnurlError(`The callback answered for ${result.paymentOption}, not ${option.id}`);
462
+ }
463
+ const target = result.kind === "bolt11" ? result.pr : result.paymentDestination;
464
+ if (!target || result.kind === "bolt11" !== (rail === "lightning")) {
465
+ throw new LnurlError(`the LNURL answered the ${rail} rail with nothing payable on it`);
466
+ }
467
+ if (rail === "onchain" && target !== own.onchain) {
468
+ throw new LnurlError("the LNURL gave an on-chain address that is not this wallet's");
469
+ }
470
+ if (rail === "arkade" && !sameOperator(target, own.arkade)) {
471
+ throw new LnurlError("the LNURL gave an Arkade address on another operator");
472
+ }
473
+ return target;
474
+ }));
475
+ }
476
+
477
+ // packages/client/src/receiver-bootstrap.ts
478
+ async function bootstrapReceiver(facade, opts = {}) {
479
+ const receiver = await facade.owned();
480
+ if (receiver?.lightningAddress) return { kind: "ready", receiver, claimed: false };
481
+ if (receiver) {
482
+ try {
483
+ return { kind: "ready", receiver, claimed: false, capabilities: await facade.capabilities() };
484
+ } catch (capabilitiesError) {
485
+ return { kind: "ready", receiver, claimed: false, capabilitiesError };
486
+ }
487
+ }
488
+ const capabilities = await facade.capabilities();
489
+ if (opts.autoClaimNameless && capabilities.allocationModes.includes("session") && !capabilities.requireApiKey) {
490
+ return { kind: "ready", receiver: await facade.claim({ nameless: true }), claimed: true, capabilities };
491
+ }
492
+ return { kind: "onboarding", capabilities };
493
+ }
494
+ async function claimOrAdopt(facade, opts) {
495
+ return await facade.owned() ?? facade.claim(opts);
496
+ }
497
+
303
498
  // packages/client/src/arkade.ts
304
499
  function isArkadeAddress(address) {
305
500
  try {
306
- ArkAddress.decode(address);
501
+ ArkAddress2.decode(address);
307
502
  return true;
308
503
  } catch {
309
504
  return false;
@@ -317,7 +512,7 @@ async function claimPublicKeyOf(identity) {
317
512
  if (key.length !== 33) {
318
513
  throw new LnurlError(`expected a 33-byte compressed public key, got ${key.length} bytes`);
319
514
  }
320
- return hex.encode(key);
515
+ return hex2.encode(key);
321
516
  }
322
517
  async function arkadeIdentityRequest(params) {
323
518
  if (!isArkadeAddress(params.arkadeAddress)) {
@@ -344,14 +539,20 @@ export {
344
539
  arkadeIdentityRequest,
345
540
  arkadeLnurl,
346
541
  arkadePaymentRouter,
542
+ bootstrapReceiver,
543
+ claimOrAdopt,
347
544
  claimPublicKeyOf,
545
+ createLnurlSendTracker,
348
546
  deriveSessionTokenForIdentity,
349
547
  encodeLnurl,
350
548
  isArkadeAddress,
351
549
  lnurlActivityResolver,
352
550
  lnurlQuoteMeta,
551
+ lnurlRailLimits,
353
552
  lnurlRails,
354
553
  mergeSentPayment,
554
+ quotedLabel,
355
555
  railOf,
556
+ resolveLnurlRails,
356
557
  sentActivityResolver
357
558
  };
@@ -0,0 +1,6 @@
1
+ import { LnurlPaymentStore } from './index.js';
2
+ import './addresses-CPAzIbJ_.js';
3
+
4
+ declare function createIndexedDbLnurlPaymentStore(dbName?: string, storeName?: string): LnurlPaymentStore;
5
+
6
+ export { createIndexedDbLnurlPaymentStore };
@@ -0,0 +1,60 @@
1
+ // packages/client/src/browser.ts
2
+ function createIndexedDbLnurlPaymentStore(dbName = "arkade-lnurl-payments", storeName = "payments") {
3
+ const open = () => new Promise((resolve, reject) => {
4
+ const request = indexedDB.open(dbName, 1);
5
+ request.onupgradeneeded = () => {
6
+ if (!request.result.objectStoreNames.contains(storeName)) {
7
+ request.result.createObjectStore(storeName, { keyPath: "key" });
8
+ }
9
+ };
10
+ request.onsuccess = () => resolve(request.result);
11
+ request.onerror = () => reject(request.error);
12
+ });
13
+ const run = (mode, work) => open().then((db) => new Promise((resolve, reject) => {
14
+ const fail = (error) => {
15
+ db.close();
16
+ reject(error ?? new DOMException("Transaction aborted", "AbortError"));
17
+ };
18
+ let started;
19
+ try {
20
+ const tx = db.transaction(storeName, mode);
21
+ started = tx;
22
+ tx.onabort = () => fail(tx.error);
23
+ const result = work(tx.objectStore(storeName));
24
+ tx.oncomplete = () => {
25
+ db.close();
26
+ try {
27
+ resolve(result());
28
+ } catch (error) {
29
+ reject(error);
30
+ }
31
+ };
32
+ } catch (error) {
33
+ if (started) {
34
+ started.onabort = () => fail(error);
35
+ try {
36
+ started.abort();
37
+ } catch {
38
+ fail(error);
39
+ }
40
+ } else fail(error);
41
+ }
42
+ }));
43
+ return {
44
+ read: () => run("readonly", (store) => {
45
+ const request = store.getAll();
46
+ return () => request.result;
47
+ }),
48
+ write: (records) => run("readwrite", (store) => {
49
+ for (const record of records) store.put(record);
50
+ return () => void 0;
51
+ }),
52
+ clear: () => run("readwrite", (store) => {
53
+ store.clear();
54
+ return () => void 0;
55
+ })
56
+ };
57
+ }
58
+ export {
59
+ createIndexedDbLnurlPaymentStore
60
+ };