@arkade-os/lnurl-client 0.0.0-stage → 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 +499 -3
- package/dist/addresses-CPAzIbJ_.d.ts +514 -0
- package/dist/arkade.d.ts +333 -0
- package/dist/arkade.js +558 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +60 -0
- package/dist/chunk-4ELYU6DY.js +1417 -0
- 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 +796 -0
- package/dist/index.js +86 -0
- package/package.json +59 -6
package/dist/arkade.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLnurlClient,
|
|
3
|
+
createPendingConfirmations,
|
|
4
|
+
deriveSessionId,
|
|
5
|
+
deriveSessionTokenWithSigner,
|
|
6
|
+
formatUnitAmount,
|
|
7
|
+
syncPayments
|
|
8
|
+
} from "./chunk-4ELYU6DY.js";
|
|
9
|
+
import {
|
|
10
|
+
LnurlError
|
|
11
|
+
} from "./chunk-H7W5RHP2.js";
|
|
12
|
+
|
|
13
|
+
// packages/client/src/arkade.ts
|
|
14
|
+
import { ArkAddress as ArkAddress2 } from "@arkade-os/sdk";
|
|
15
|
+
import { hex as hex2 } from "@scure/base";
|
|
16
|
+
|
|
17
|
+
// packages/client/src/rail.ts
|
|
18
|
+
import { isLnurl } from "@arkade-os/sdk";
|
|
19
|
+
var LNURL_ARKADE_RAIL = "lnurl-arkade";
|
|
20
|
+
var LNURL_LIGHTNING_RAIL = "lnurl-lightning";
|
|
21
|
+
var PAY_REQUEST_TTL_MS = 3e4;
|
|
22
|
+
function lnurlQuoteMeta(quote) {
|
|
23
|
+
const meta = quote.meta?.lnurl;
|
|
24
|
+
return typeof meta?.target === "string" && typeof meta.via === "string" ? meta : void 0;
|
|
25
|
+
}
|
|
26
|
+
function lnurlRails(deps) {
|
|
27
|
+
const ttlMs = deps.ttlMs ?? PAY_REQUEST_TTL_MS;
|
|
28
|
+
const cache = /* @__PURE__ */ new Map();
|
|
29
|
+
const resolveOnce = (raw) => {
|
|
30
|
+
const key = raw.trim();
|
|
31
|
+
const hit = cache.get(key);
|
|
32
|
+
if (hit && Date.now() - hit.at < ttlMs) return hit.pr;
|
|
33
|
+
const pr = deps.client.resolve(key).catch((err) => {
|
|
34
|
+
cache.delete(key);
|
|
35
|
+
throw err;
|
|
36
|
+
});
|
|
37
|
+
cache.set(key, { at: Date.now(), pr });
|
|
38
|
+
return pr;
|
|
39
|
+
};
|
|
40
|
+
const rails = [makeRail("arkade", LNURL_ARKADE_RAIL, deps.arkade, deps, resolveOnce)];
|
|
41
|
+
if (deps.lightning) {
|
|
42
|
+
rails.push(makeRail("lightning", LNURL_LIGHTNING_RAIL, deps.lightning, deps, resolveOnce));
|
|
43
|
+
}
|
|
44
|
+
return rails;
|
|
45
|
+
}
|
|
46
|
+
function makeRail(type, id, inner, deps, resolveOnce) {
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
// The SDK's own predicate, which is deliberately looser than this package's
|
|
50
|
+
// isValidLnUrl: classification is format-only and synchronous, so the rail
|
|
51
|
+
// claims the shape here and lets resolve() in available() reject a bad
|
|
52
|
+
// checksum. Using it is also what finally gives `isLnurl` a consumer.
|
|
53
|
+
match: (req) => isLnurl(req.raw.trim()),
|
|
54
|
+
async available(req) {
|
|
55
|
+
let pr;
|
|
56
|
+
try {
|
|
57
|
+
pr = await resolveOnce(req.raw);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
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
|
+
}
|
|
66
|
+
if (pr.paymentOptions === void 0) {
|
|
67
|
+
if (type !== "lightning") return false;
|
|
68
|
+
if (req.amount === void 0) return true;
|
|
69
|
+
const msat2 = req.amount * 1e3;
|
|
70
|
+
return msat2 >= pr.minSendable && msat2 <= pr.maxSendable;
|
|
71
|
+
}
|
|
72
|
+
const option = pr.paymentOptions.find((o) => o.type === type && o.available !== false);
|
|
73
|
+
if (!option) return false;
|
|
74
|
+
if (req.amount === void 0) return true;
|
|
75
|
+
const msat = req.amount * 1e3;
|
|
76
|
+
return msat >= (option.minSendable ?? pr.minSendable) && msat <= (option.maxSendable ?? pr.maxSendable);
|
|
77
|
+
},
|
|
78
|
+
async quote(req, ctx) {
|
|
79
|
+
const pr = await resolveOnce(req.raw);
|
|
80
|
+
const inUnit = req.unitAmount;
|
|
81
|
+
if (req.amount === void 0 && !inUnit) throw new LnurlError(`${id} needs an explicit amount`);
|
|
82
|
+
const option = pr.paymentOptions?.find((o) => o.type === type && o.available !== false)?.id;
|
|
83
|
+
const extra = {
|
|
84
|
+
...option !== void 0 ? { paymentOption: option } : {},
|
|
85
|
+
...deps.comment !== void 0 ? { comment: deps.comment } : {}
|
|
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 };
|
|
94
|
+
if (!inner.match(delegated, ctx)) {
|
|
95
|
+
throw new LnurlError(`${id}: ${inner.id} cannot pay the destination the callback returned`);
|
|
96
|
+
}
|
|
97
|
+
const quote = await inner.quote(delegated, ctx);
|
|
98
|
+
const expect = result.kind === "bolt11" ? { pr: result.pr } : { paymentOption: result.paymentOption, paymentDestination: result.paymentDestination };
|
|
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
|
+
};
|
|
108
|
+
return { ...quote, railId: id, meta: { ...quote.meta, lnurl } };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function unitOf(pr, code) {
|
|
113
|
+
return pr.units?.find((u) => u.code.toUpperCase() === code.toUpperCase());
|
|
114
|
+
}
|
|
115
|
+
function destinationOf(destination, id) {
|
|
116
|
+
if (!destination) throw new LnurlError(`${id}: the callback returned no payment destination`);
|
|
117
|
+
return destination;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// packages/client/src/wallet.ts
|
|
121
|
+
import { bech32 } from "@scure/base";
|
|
122
|
+
|
|
123
|
+
// packages/client/src/sdk-adapter.ts
|
|
124
|
+
import { arkRail, BIP21, createDefaultPaymentRouter } from "@arkade-os/sdk";
|
|
125
|
+
function arkadePaymentRouter(opts) {
|
|
126
|
+
const router = opts.router ?? createDefaultPaymentRouter(opts.wallet);
|
|
127
|
+
if (opts.lightning) router.use(invoicePricedLeg(opts.lightning));
|
|
128
|
+
for (const rail of lnurlRails({
|
|
129
|
+
client: opts.client ?? createLnurlClient(),
|
|
130
|
+
arkade: arkRail(),
|
|
131
|
+
...opts.lightning ? { lightning: opts.lightning } : {},
|
|
132
|
+
...opts.comment !== void 0 ? { comment: opts.comment } : {}
|
|
133
|
+
})) {
|
|
134
|
+
router.use(rail);
|
|
135
|
+
}
|
|
136
|
+
return router;
|
|
137
|
+
}
|
|
138
|
+
function invoicePricedLeg(rail) {
|
|
139
|
+
const leg = (req) => {
|
|
140
|
+
if (req.amount === void 0 || req.amount !== BIP21.amountSats(req.raw)) return req;
|
|
141
|
+
const { amount: _restated, ...rest } = req;
|
|
142
|
+
return rest;
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
id: rail.id,
|
|
146
|
+
match: (req, ctx) => rail.match(req, ctx),
|
|
147
|
+
available: (req, ctx) => rail.available?.(leg(req), ctx) ?? true,
|
|
148
|
+
quote: (req, ctx) => rail.quote(leg(req), ctx)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// packages/client/src/wallet.ts
|
|
153
|
+
function encodeLnurl(url) {
|
|
154
|
+
return bech32.encode("lnurl", bech32.toWords(new TextEncoder().encode(url)), 1023).toUpperCase();
|
|
155
|
+
}
|
|
156
|
+
var DEFAULT_RAIL_PRIORITY = [
|
|
157
|
+
"lnurl-arkade",
|
|
158
|
+
"ark",
|
|
159
|
+
"ark-asset",
|
|
160
|
+
"lnurl-lightning",
|
|
161
|
+
"solver-lightning",
|
|
162
|
+
"onchain"
|
|
163
|
+
];
|
|
164
|
+
function arkadeLnurl(opts) {
|
|
165
|
+
const { wallet, baseUrl } = opts;
|
|
166
|
+
const domain = opts.domain ?? new URL(baseUrl).hostname;
|
|
167
|
+
const identity = wallet ? wallet.identity : opts.identity;
|
|
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
|
+
};
|
|
173
|
+
const client = opts.client ?? createLnurlClient({ baseUrl });
|
|
174
|
+
const payer = createLnurlClient();
|
|
175
|
+
let tokenOnce;
|
|
176
|
+
const token = () => tokenOnce ??= deriveSessionTokenForIdentity(identity, domain);
|
|
177
|
+
let routerOnce;
|
|
178
|
+
const router = () => {
|
|
179
|
+
if (!wallet) throw new LnurlError("paying needs a wallet; arkadeLnurl was given an identity only");
|
|
180
|
+
return routerOnce ??= arkadePaymentRouter({
|
|
181
|
+
wallet,
|
|
182
|
+
client: payer,
|
|
183
|
+
...opts.router ? { router: opts.router } : {},
|
|
184
|
+
...opts.lightningRail ? { lightning: opts.lightningRail } : {}
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
let writes = 0;
|
|
188
|
+
const wrote = () => {
|
|
189
|
+
writes++;
|
|
190
|
+
};
|
|
191
|
+
const needStore = () => {
|
|
192
|
+
if (!opts.store) throw new LnurlError("sync needs a store; pass one to arkadeLnurl");
|
|
193
|
+
return opts.store;
|
|
194
|
+
};
|
|
195
|
+
const receiverFrom = (held, handle, lightningAddress, flagged) => {
|
|
196
|
+
const lnurl = flagged ? encodeLnurl(`${baseUrl}/lnurl/${deriveSessionId(held)}`) : encodeLnurl(`${baseUrl}/.well-known/lnurlp/${handle}`);
|
|
197
|
+
let memo;
|
|
198
|
+
return {
|
|
199
|
+
handle,
|
|
200
|
+
lightningAddress: lightningAddress ?? void 0,
|
|
201
|
+
lnurl,
|
|
202
|
+
// `resolve` ignores `baseUrl` and sends no token, so one client is enough.
|
|
203
|
+
payRequest() {
|
|
204
|
+
if (memo && memo.writes === writes && Date.now() - memo.at < PAY_REQUEST_TTL_MS) return memo.pr;
|
|
205
|
+
const entry = { at: Date.now(), writes, pr: client.resolve(lnurl) };
|
|
206
|
+
memo = entry;
|
|
207
|
+
entry.pr.catch(() => {
|
|
208
|
+
if (memo === entry) memo = void 0;
|
|
209
|
+
});
|
|
210
|
+
return entry.pr;
|
|
211
|
+
},
|
|
212
|
+
payments: (listOpts) => client.listPayments(held, handle, { domain, ...listOpts }),
|
|
213
|
+
allocations: (listOpts) => client.listHdAllocations(held, handle, { domain, ...listOpts }),
|
|
214
|
+
sync: async () => syncPayments(
|
|
215
|
+
[{ baseUrl, token: held, handle, domain }],
|
|
216
|
+
{ client: (target) => createLnurlClient({ baseUrl: target }), store: needStore() }
|
|
217
|
+
),
|
|
218
|
+
async upgrade(upgradeOpts) {
|
|
219
|
+
const registered = await client.upgradeAddress({ token: held, handle, domain, ...upgradeOpts }).finally(wrote);
|
|
220
|
+
return receiverFrom(held, registered.handle, registered.lightningAddress, flagged);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
token,
|
|
226
|
+
capabilities: () => client.domainCapabilities({ domain }),
|
|
227
|
+
async owned() {
|
|
228
|
+
const held = await token();
|
|
229
|
+
const mine = (await client.listAddresses(held)).filter((a) => a.domain.toLowerCase() === domain.toLowerCase());
|
|
230
|
+
const active = mine.filter((a) => a.status === "active");
|
|
231
|
+
const chosen = active.find((a) => a.sessionLnurl !== null) ?? active[0] ?? mine[0];
|
|
232
|
+
return chosen ? receiverFrom(held, chosen.handle, chosen.lightningAddress, chosen.sessionLnurl !== null) : void 0;
|
|
233
|
+
},
|
|
234
|
+
async claim(claimOpts) {
|
|
235
|
+
const held = await token();
|
|
236
|
+
const registered = await client.registerAddress({
|
|
237
|
+
token: held,
|
|
238
|
+
domain,
|
|
239
|
+
...claimOpts?.nameless ? { nameless: true } : {},
|
|
240
|
+
...claimOpts?.username !== void 0 ? { username: claimOpts.username } : {},
|
|
241
|
+
...claimOpts?.claimCode !== void 0 ? { claimCode: claimOpts.claimCode } : {}
|
|
242
|
+
}).finally(wrote);
|
|
243
|
+
const flagged = registered.sessionLnurl != null;
|
|
244
|
+
if (claimOpts?.nameless && !flagged) {
|
|
245
|
+
throw new LnurlError(
|
|
246
|
+
`server ignored nameless and registered "${registered.handle}"; it predates nameless receivers \u2014 revoke that address if unwanted`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
await client.registerArkadeIdentity(
|
|
250
|
+
opts.hd ? { token: held, handle: registered.handle, domain, hd: opts.hd } : await arkadeIdentityRequest({
|
|
251
|
+
identity,
|
|
252
|
+
token: held,
|
|
253
|
+
handle: registered.handle,
|
|
254
|
+
domain,
|
|
255
|
+
...await addresses()
|
|
256
|
+
})
|
|
257
|
+
).finally(wrote);
|
|
258
|
+
return receiverFrom(held, registered.handle, registered.lightningAddress, flagged);
|
|
259
|
+
},
|
|
260
|
+
router,
|
|
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
|
+
)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// packages/client/src/activity.ts
|
|
269
|
+
var quotedLabel = (q) => `${formatUnitAmount(q.amount, q.decimals)} ${q.unit}`;
|
|
270
|
+
var LNURL_RESOLVER_ID = "lnurl:payments";
|
|
271
|
+
var SENT_RESOLVER_ID = "lnurl:sends";
|
|
272
|
+
var LNURL_GROUP_PREFIX = "lnurl:";
|
|
273
|
+
var SENT_GROUP_PREFIX = "sent:";
|
|
274
|
+
var railOf = (p) => p.kind === "bolt11" ? "lightning" : p.paymentOption ?? "destination";
|
|
275
|
+
function absorbedPaymentKey(activityId) {
|
|
276
|
+
return activityId.startsWith(LNURL_GROUP_PREFIX) ? activityId.slice(LNURL_GROUP_PREFIX.length) : void 0;
|
|
277
|
+
}
|
|
278
|
+
function mergeSentPayment(existing, incoming) {
|
|
279
|
+
const byTxid = new Map(existing.map((s) => [s.txid, s]));
|
|
280
|
+
byTxid.set(incoming.txid, { ...byTxid.get(incoming.txid), ...incoming });
|
|
281
|
+
return [...byTxid.values()];
|
|
282
|
+
}
|
|
283
|
+
function sentActivityResolver(sends) {
|
|
284
|
+
let byTxid = /* @__PURE__ */ new Map();
|
|
285
|
+
return {
|
|
286
|
+
id: SENT_RESOLVER_ID,
|
|
287
|
+
prepare: async () => {
|
|
288
|
+
byTxid = new Map(sends().filter((s) => s.txid).map((s) => [s.txid, s]));
|
|
289
|
+
},
|
|
290
|
+
resolve: (tx) => {
|
|
291
|
+
const sent = byTxid.get(tx.key.arkTxid);
|
|
292
|
+
if (!sent) return void 0;
|
|
293
|
+
return [{
|
|
294
|
+
groupId: `${SENT_GROUP_PREFIX}${sent.txid}`,
|
|
295
|
+
// The counterparty, not the rail: that is the question a sent row answers.
|
|
296
|
+
label: `\u2192 ${sent.target}`,
|
|
297
|
+
kind: "lnurl-send",
|
|
298
|
+
metadata: {
|
|
299
|
+
target: sent.target,
|
|
300
|
+
rail: sent.railId,
|
|
301
|
+
delivered: `${sent.amountSat} sats`,
|
|
302
|
+
...sent.quote ? { requested: quotedLabel(sent.quote) } : {},
|
|
303
|
+
...sent.feeSat > 0 ? { fee: `${sent.feeSat} sats` } : {},
|
|
304
|
+
...sent.swapId ? { swap: sent.swapId } : {},
|
|
305
|
+
...sent.preimage ? { preimage: sent.preimage } : {},
|
|
306
|
+
...sent.receiverConfirmed === void 0 ? {} : { receiver: sent.receiverConfirmed ? "confirmed settled" : "has not confirmed" }
|
|
307
|
+
}
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function lnurlActivityResolver(payments) {
|
|
313
|
+
let byTxid = /* @__PURE__ */ new Map();
|
|
314
|
+
return {
|
|
315
|
+
id: LNURL_RESOLVER_ID,
|
|
316
|
+
prepare: async () => {
|
|
317
|
+
byTxid = new Map(
|
|
318
|
+
payments().filter((p) => Boolean(p.payoutReference)).map((p) => [p.payoutReference, p])
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
resolve: (tx) => {
|
|
322
|
+
const payment = byTxid.get(tx.key.arkTxid);
|
|
323
|
+
if (!payment) return void 0;
|
|
324
|
+
return [{
|
|
325
|
+
groupId: `${LNURL_GROUP_PREFIX}${payment.key}`,
|
|
326
|
+
label: `${railOf(payment)} \xB7 ${payment.lightningAddress ?? "LNURL"}`,
|
|
327
|
+
kind: "lnurl",
|
|
328
|
+
metadata: {
|
|
329
|
+
rail: railOf(payment),
|
|
330
|
+
lightningAddress: payment.lightningAddress,
|
|
331
|
+
identifier: payment.identifier,
|
|
332
|
+
verified: payment.settled,
|
|
333
|
+
...payment.quote ? { requested: quotedLabel(payment.quote) } : {}
|
|
334
|
+
}
|
|
335
|
+
}];
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
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
|
+
|
|
498
|
+
// packages/client/src/arkade.ts
|
|
499
|
+
function isArkadeAddress(address) {
|
|
500
|
+
try {
|
|
501
|
+
ArkAddress2.decode(address);
|
|
502
|
+
return true;
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function deriveSessionTokenForIdentity(identity, domain) {
|
|
508
|
+
return deriveSessionTokenWithSigner((message) => identity.signMessage(message, "ecdsa"), domain);
|
|
509
|
+
}
|
|
510
|
+
async function claimPublicKeyOf(identity) {
|
|
511
|
+
const key = await identity.compressedPublicKey();
|
|
512
|
+
if (key.length !== 33) {
|
|
513
|
+
throw new LnurlError(`expected a 33-byte compressed public key, got ${key.length} bytes`);
|
|
514
|
+
}
|
|
515
|
+
return hex2.encode(key);
|
|
516
|
+
}
|
|
517
|
+
async function arkadeIdentityRequest(params) {
|
|
518
|
+
if (!isArkadeAddress(params.arkadeAddress)) {
|
|
519
|
+
throw new LnurlError(`not a valid Arkade address: ${params.arkadeAddress}`);
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
token: params.token,
|
|
523
|
+
handle: params.handle,
|
|
524
|
+
arkadeAddress: params.arkadeAddress,
|
|
525
|
+
claimPublicKey: await claimPublicKeyOf(params.identity),
|
|
526
|
+
...params.boardingAddress !== void 0 ? { boardingAddress: params.boardingAddress } : {},
|
|
527
|
+
...params.domain !== void 0 ? { domain: params.domain } : {}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
export {
|
|
531
|
+
DEFAULT_RAIL_PRIORITY,
|
|
532
|
+
LNURL_ARKADE_RAIL,
|
|
533
|
+
LNURL_GROUP_PREFIX,
|
|
534
|
+
LNURL_LIGHTNING_RAIL,
|
|
535
|
+
LNURL_RESOLVER_ID,
|
|
536
|
+
SENT_GROUP_PREFIX,
|
|
537
|
+
SENT_RESOLVER_ID,
|
|
538
|
+
absorbedPaymentKey,
|
|
539
|
+
arkadeIdentityRequest,
|
|
540
|
+
arkadeLnurl,
|
|
541
|
+
arkadePaymentRouter,
|
|
542
|
+
bootstrapReceiver,
|
|
543
|
+
claimOrAdopt,
|
|
544
|
+
claimPublicKeyOf,
|
|
545
|
+
createLnurlSendTracker,
|
|
546
|
+
deriveSessionTokenForIdentity,
|
|
547
|
+
encodeLnurl,
|
|
548
|
+
isArkadeAddress,
|
|
549
|
+
lnurlActivityResolver,
|
|
550
|
+
lnurlQuoteMeta,
|
|
551
|
+
lnurlRailLimits,
|
|
552
|
+
lnurlRails,
|
|
553
|
+
mergeSentPayment,
|
|
554
|
+
quotedLabel,
|
|
555
|
+
railOf,
|
|
556
|
+
resolveLnurlRails,
|
|
557
|
+
sentActivityResolver
|
|
558
|
+
};
|
package/dist/browser.js
ADDED
|
@@ -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
|
+
};
|