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