@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/README.md
CHANGED
|
@@ -1,3 +1,362 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @arkade-os/lnurl-client
|
|
2
|
+
|
|
3
|
+
Full-duplex client for the lnurl-server protocol: payer (`resolve`, `requestInvoice`, `pollVerify`) and receiver (sessions, settled reporting, address and Arkade-identity registration).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @arkade-os/lnurl-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The SDK is an optional peer: `>=0.5.0-rc.0` (0.5.0 release candidates and stable 0.5+ releases). The main entry point needs no SDK; `/arkade` uses the SDK installed by your app.
|
|
12
|
+
|
|
13
|
+
Runtime dependencies are `@scure/base` and `@noble/hashes` - nothing else is pulled in, and the package never imports the server, so app bundles stay free of `express` and `sqlite`.
|
|
14
|
+
|
|
15
|
+
## With an Arkade wallet
|
|
16
|
+
|
|
17
|
+
If you hold an `@arkade-os/sdk` wallet, start here. `arkadeLnurl` takes the wallet and the server root and does the rest - token derivation, claiming the address, binding the identity, the sync loop, and the send rails:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { arkadeLnurl } from '@arkade-os/lnurl-client/arkade'
|
|
21
|
+
import { browserPaymentStore } from '@arkade-os/lnurl-client'
|
|
22
|
+
|
|
23
|
+
const lnurl = arkadeLnurl({ wallet, baseUrl: 'https://lnurl.example.com', store: browserPaymentStore() })
|
|
24
|
+
|
|
25
|
+
// Receive: claims the name AND binds the identity, which is what makes the
|
|
26
|
+
// address advertise any paymentOptions at all. Omit the name for a
|
|
27
|
+
// server-assigned one, or pass `{ nameless: true }` for no address at all -
|
|
28
|
+
// just the stable `lnurl` below.
|
|
29
|
+
const rx = await lnurl.claim({ username: 'alice' }) // rx.lightningAddress === 'alice@lnurl.example.com'
|
|
30
|
+
await rx.sync() // pull the server's own records
|
|
31
|
+
|
|
32
|
+
// Send: the router already knows how to pay a Lightning address.
|
|
33
|
+
const [best] = await lnurl.options('bob@example.com', 1000)
|
|
34
|
+
const handle = await (await best.quote()).send()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`wallet.identity` supplies the signing, so nothing else is passed in. The token is derived once and memoised - it signs twice on first use to prove the signer is deterministic, and a second call costs nothing. `claim` and `owned` return a `Receiver`, scoped to one address:
|
|
38
|
+
|
|
39
|
+
| you want | call |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| what this domain allows | `capabilities()` |
|
|
42
|
+
| the address this wallet already owns here | `owned()` |
|
|
43
|
+
| claim a name (or none) and bind the identity | `claim(opts?)` |
|
|
44
|
+
| its `user@domain`, or `undefined` when nameless | `receiver.lightningAddress` |
|
|
45
|
+
| an LNURL for a QR | `receiver.lnurl` |
|
|
46
|
+
| its own payRequest | `receiver.payRequest()` |
|
|
47
|
+
| one page of the server's records | `receiver.payments(opts)` |
|
|
48
|
+
| everything new, into your store | `receiver.sync()` |
|
|
49
|
+
| name a nameless receiver in place | `receiver.upgrade(opts?)` |
|
|
50
|
+
| rank the ways to pay a target | `options(target, amountSat)` |
|
|
51
|
+
| the router itself | `router()` |
|
|
52
|
+
|
|
53
|
+
**`payRequest()` fetches once per 30 s.** Every call on one `Receiver` within that window shares a single fetch, in flight or done, so a screen asking on every amount change costs one GET; a failed fetch is not kept. `claim` and `upgrade` retire it at once, even when they fail; a change made anywhere else - another device, the operator - shows within 30 s. `createLnurlClient().resolve(receiver.lnurl)` always asks the server.
|
|
54
|
+
|
|
55
|
+
`capabilities()` reads what a domain allows before you call it - `{ domain, allocationModes, usernameRules, requireApiKey }` - so onboarding can offer only the modes actually enabled instead of guessing and hitting `forbidden_mode`. A domain requiring an operator-reserved name takes a `claimCode` the same way on either call below: `claim({ username: 'alice', claimCode })`.
|
|
56
|
+
|
|
57
|
+
**Nameless, and upgrading later.** `claim({ nameless: true })` returns a `Receiver` with no lightning address at all - offline receive, rails and payment sync all work off its stable `lnurl` alone. `receiver.upgrade({ username: 'alice' })` (or `{ username, claimCode }` for a reserved name) names it in place: the wallet keeps its history and the same `lnurl` keeps paying it, but the upgrade **publicly links** that LNURL to the new name. To keep them unlinked instead, call `lnurl.claim({ username: 'alice' })` on the side - a second, separate named address (counted against the domain's per-wallet limit) with no connection to the nameless one.
|
|
58
|
+
|
|
59
|
+
Paying needs no address of your own, so the router is available separately - `arkadePaymentRouter({ wallet, lightning })` returns the SDK router with the LNURL rails registered, which is what makes `route()` accept a Lightning address instead of throwing `no rail for`.
|
|
60
|
+
|
|
61
|
+
Receiving accepts a `LnurlWallet`: only `identity.signMessage`, `identity.compressedPublicKey`, `getAddress`, and `getBoardingAddress` are required. It does not require the SDK's concrete `Wallet` class. For sending, the default adapter expects a full SDK spending wallet. Custom wallets can pass a `LnurlPaymentRouter` as `router` to either `arkadeLnurl` or `arkadePaymentRouter`; the adapter registers the LNURL rails on that router. `LnurlPaymentRouter` requires only `use`, `remove`, `options`, and `route`, without the SDK class's private members. SDK rails and router data types remain part of the `/arkade` contract.
|
|
62
|
+
|
|
63
|
+
CI checks the client against SDK `0.5.0-rc.0` and `0.5.0-rc.19` in isolated installs, independent of the server's SDK pin. Run the same types, tests, build, and consumer checks with `node scripts/check-client-sdk-compat.mjs <sdk-version>` from the repository root.
|
|
64
|
+
|
|
65
|
+
Everything below is the surface these are built on; reach for it when the facade does not fit.
|
|
66
|
+
|
|
67
|
+
## Payer
|
|
68
|
+
|
|
69
|
+
`createLnurlClient()` with no `baseUrl` covers payer-only use: `resolve`, `requestInvoice` and `pollVerify` talk to whatever host the address or LNURL points at.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createLnurlClient } from "@arkade-os/lnurl-client";
|
|
73
|
+
|
|
74
|
+
const payer = createLnurlClient();
|
|
75
|
+
|
|
76
|
+
const payRequest = await payer.resolve("alice@example.com"); // or an lnurl bech32 string
|
|
77
|
+
const invoice = await payer.requestInvoice(payRequest, { amountSat: 50 });
|
|
78
|
+
|
|
79
|
+
// Pay invoice.pr through your own Lightning path, then wait for settlement:
|
|
80
|
+
if (invoice.kind === "bolt11" && invoice.verify) {
|
|
81
|
+
const status = await payer.pollVerify(invoice.verify, { timeoutMs: 120_000, intervalMs: 1_000, expect: invoice });
|
|
82
|
+
if (status.kind === "bolt11" && status.settled) {
|
|
83
|
+
console.log("settled, preimage:", status.preimage);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A settled bolt11 status is only returned when `SHA256(preimage)` equals the payment hash in `pr`. Anything else — a wrong preimage, or a `pr` that cannot be decoded — is rejected rather than reported as settled.
|
|
89
|
+
|
|
90
|
+
That proves the answer's own `pr`, not that it is the invoice you are watching. Pass the callback's answer as `expect` and an answer about any other payment — another invoice with its genuine preimage, a destination status, another destination — fails with `LnurlError` instead. lnurl rails record the same value on their quotes as `lnurlQuoteMeta(quote).expect`; for batched or streamed answers, `verifyStatusIsFor(status, expect)` is the same check.
|
|
91
|
+
|
|
92
|
+
`requestInvoice` takes `amountSat` plus an optional `comment`. It also accepts `paymentOption` and `unit`, except on a session payRequest that does not advertise them (a live session's); a nameless receiver's session LNURL advertises both. The amount is range-checked locally before anything hits the wire.
|
|
93
|
+
|
|
94
|
+
**Rails can carry different amounts, so check the option you selected.** A covenant destination is bounded by dust and VTXO shape, a solver-mediated swap by whatever the solver quotes — so an entry in `paymentOptions` may publish its own `minSendable`/`maxSendable`, and those win over the top-level pair when you select it. The top-level pair describes the rail you get by sending no `paymentOption` at all. `requestInvoice` applies that rule for you; apply it yourself if you build the callback URL by hand, or you will reject amounts the rail would have accepted.
|
|
95
|
+
|
|
96
|
+
### Many pending invoices: verifyBatch
|
|
97
|
+
|
|
98
|
+
When the callback also returns `verifyBatch` ([LUD-XX, lnurl/luds#299](https://github.com/lnurl/luds/pull/299)), one request covers every pending invoice at that endpoint:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const { results } = await payer.batchVerify(invoice.verifyBatch, pendingVerifyUrls);
|
|
102
|
+
// results[url] is { kind: "verify", status } or { kind: "error", reason }, one per URL you sent
|
|
103
|
+
|
|
104
|
+
const stream = payer.openVerifyBatchStream(
|
|
105
|
+
{ verifyBatchUrl: invoice.verifyBatch, verifyUrls: pendingVerifyUrls },
|
|
106
|
+
{ onUpdate: (url, status) => { /* snapshot first, then live settlements */ }, onClose: () => { /* reopen if still pending */ } },
|
|
107
|
+
);
|
|
108
|
+
stream.update([newVerifyUrl], [settledVerifyUrl]); // grow or shrink the set without reconnecting
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`batchVerify` splits the set and retries when the server answers `414`/`431`. The stream falls back to the one-shot snapshot when the server does not stream (including a `406`), and closing is final: reopening is the caller's job.
|
|
112
|
+
|
|
113
|
+
### Token options
|
|
114
|
+
|
|
115
|
+
A payRequest may offer a token on another chain (an lnurl-server with FixedFloat deposits, say): an option whose `type` is a CAIP-2 namespace, with a CAIP-19 `asset`, a `unit` naming a `units[]` entry, and the `provider` the payer hands the tokens to. Read them with `tokenOptions`, which accepts exactly what the BTCPay LNURLVerify plugin accepts:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { tokenOptions } from "@arkade-os/lnurl-client";
|
|
119
|
+
|
|
120
|
+
for (const option of tokenOptions(payRequest)) {
|
|
121
|
+
const answer = await payer.requestInvoice(payRequest, { amountSat: 10_000, paymentOption: option.id });
|
|
122
|
+
if (answer.kind !== "destination") continue;
|
|
123
|
+
// answer.paymentURI (EIP-681 or Solana Pay; absent on Tron), answer.paymentDestination,
|
|
124
|
+
// answer.paymentQuote.payment = { amount: base units, unit: option.unit.code },
|
|
125
|
+
// answer.paymentQuote.expiresAt = the payer's deadline (ISO 8601)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`requestInvoice` refuses a token answer for another option, one whose destination is not an address on the token's chain, and one that quotes no whole amount of the token. The amount you request is still sats: what the receiver is paid before the swap's fee.
|
|
130
|
+
|
|
131
|
+
## Receiver
|
|
132
|
+
|
|
133
|
+
Receiver and management calls need `baseUrl`. `deriveSessionToken` turns a wallet private key and a domain into a stable session token; opening a session with the same token reconnects the same session id.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createLnurlClient, deriveSessionToken } from "@arkade-os/lnurl-client";
|
|
137
|
+
|
|
138
|
+
const receiver = createLnurlClient({ baseUrl: "https://lnurl.example.com" });
|
|
139
|
+
const token = deriveSessionToken("<wallet-private-key-hex>", "example.com");
|
|
140
|
+
|
|
141
|
+
const session = await receiver.openSession({ token }, {
|
|
142
|
+
onInvoiceRequest: async ({ amountMsat }, respond) => {
|
|
143
|
+
const pr = await makeInvoice(amountMsat); // injected invoice provider: your Lightning node
|
|
144
|
+
await respond.answerInvoice(pr); // or: await respond.rejectInvoice(reason)
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// session.lnurl is what payers resolve. Once your node reports the invoice settled:
|
|
149
|
+
await session.reportSettled("<preimage-hex>");
|
|
150
|
+
session.close();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Deriving the token without handing over your key
|
|
154
|
+
|
|
155
|
+
A library should not need your private key, so there are two ways to derive the token and **they produce the same value** — a wallet can move between them without losing its addresses:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { deriveSessionToken, deriveSessionTokenWithSigner } from '@arkade-os/lnurl-client'
|
|
159
|
+
|
|
160
|
+
// Preferred: the key never leaves the wallet.
|
|
161
|
+
const token = await deriveSessionTokenWithSigner(
|
|
162
|
+
(msg, type) => identity.signMessage(msg, type), // @arkade-os/ts-sdk Identity
|
|
163
|
+
'example.com',
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Equivalent, for callers that already hold raw key material:
|
|
167
|
+
const same = deriveSessionToken('<private-key-hex>', 'example.com')
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
They agree because the token is `sha256` of a **deterministic ECDSA signature** over `sha256("lnurl-session:<domain>")` — a pure function of key and message — and `deriveSessionToken` simply performs that signature itself.
|
|
171
|
+
|
|
172
|
+
**It must be ECDSA, and the function passes the type for you.** BIP-340 schnorr is randomised unless given an explicit aux, which `Identity.signMessage` does not expose — and schnorr is its *default*. A schnorr signature is also 64 bytes, exactly like compact ECDSA, so nothing about the returned bytes reveals the mistake; it would simply mint a new token on every call and silently orphan the address. `deriveSessionTokenWithSigner` therefore signs twice and throws if the results differ, rather than trusting the wiring.
|
|
173
|
+
|
|
174
|
+
**The token is per-domain, and that is a security property, not bookkeeping.** It is a bearer credential: the server receives it and stores it (encrypted, but a server it is stored on can read it). Were it derived from the private key alone, the same credential would authenticate its holder at *every* lnurl-server the user has ever used — so one malicious or breached server could call `registerArkadeIdentity` on a different server and repoint the victim's receive address, which the covenant would then faithfully pay. Binding the domain into the derivation makes a token minted for `example.com` useless at `other.com`.
|
|
175
|
+
|
|
176
|
+
Two consequences worth planning for:
|
|
177
|
+
|
|
178
|
+
- A wallet using several servers holds **several tokens**, one per domain. Derive per domain rather than caching one.
|
|
179
|
+
- The server cannot enforce this — it sees an opaque token and cannot tell which domain produced it — so the protection holds only for clients that derive this way.
|
|
180
|
+
|
|
181
|
+
Derive the token from wallet key material rather than generating a random one you then have to persist: losing it loses the address, since the server identifies the owner by `sha256` of the token bytes and has no other record of who you are.
|
|
182
|
+
|
|
183
|
+
`deriveSessionId(token)` computes that session id locally — the same value the server derives — which is useful for asserting a reconnect resumed the session you expected rather than trusting the id echoed back to you.
|
|
184
|
+
|
|
185
|
+
## Validating input
|
|
186
|
+
|
|
187
|
+
Exported standalone, because a UI validates what a user pasted or scanned before it has a client or a `baseUrl`:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { isValidLnUrl, isLnAddress, isLnUrl, toPayRequestUrl } from '@arkade-os/lnurl-client'
|
|
191
|
+
|
|
192
|
+
isValidLnUrl('alice@example.com') // true — either form
|
|
193
|
+
isValidLnUrl('LNURL1DP68…') // true
|
|
194
|
+
isValidLnUrl('not an lnurl') // false
|
|
195
|
+
|
|
196
|
+
toPayRequestUrl('alice@example.com')
|
|
197
|
+
// → { url: 'https://example.com/.well-known/lnurlp/alice', surface: 'address' }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`isLnAddress` and `isLnUrl` test one form each; `isValidLnUrl` accepts either. All are shape checks only — they never touch the network, so a well-formed but unregistered address passes here and fails at `resolve`.
|
|
201
|
+
|
|
202
|
+
`toPayRequestUrl` also reports the **surface**. A live `session` LNURL takes an amount and a comment and nothing else, so `requestInvoice` rejects rail options on a session payRequest that advertises none, rather than letting the server ignore them silently. A nameless receiver's session LNURL serves its address, `paymentOptions` and units included, and is paid like one.
|
|
203
|
+
|
|
204
|
+
Mixed-case LNURLs are rejected per BIP-173 — bech32 forbids mixed case so that case-mangling in transit cannot slip past the checksum. All-uppercase, which is what QR codes carry, decodes normally.
|
|
205
|
+
|
|
206
|
+
## Lightning addresses
|
|
207
|
+
|
|
208
|
+
A LUD-16 address (`alice@example.com`) is payable whether or not the wallet is online. Registering one is what unlocks the offline rails — without it a payer can only reach a live session.
|
|
209
|
+
|
|
210
|
+
**The token is the credential for everything here.** The server derives the address's owner from it (`sha256` of the token bytes), so the same token that opens a session also owns the addresses registered with it. Losing it loses the address; leaking it hands someone else control of where payments go. Derive it from wallet key material with `deriveSessionToken` rather than generating a fresh random one you then have to store.
|
|
211
|
+
|
|
212
|
+
All calls in this section need `baseUrl`.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createLnurlClient, deriveSessionToken } from '@arkade-os/lnurl-client'
|
|
216
|
+
|
|
217
|
+
const client = createLnurlClient({ baseUrl: 'https://lnurl.example.com' })
|
|
218
|
+
// Per-domain: this token authenticates at example.com and nowhere else.
|
|
219
|
+
const token = deriveSessionToken('<wallet-private-key-hex>', 'example.com')
|
|
220
|
+
|
|
221
|
+
const address = await client.registerAddress({ token, username: 'alice' })
|
|
222
|
+
// → { lightningAddress: 'alice@example.com', lnurl: 'LNURL1…', username, domain, status }
|
|
223
|
+
|
|
224
|
+
const mine = await client.listAddresses(token)
|
|
225
|
+
// → [{ username, domain, status, createdAt, lightningAddress, lnurl }]
|
|
226
|
+
|
|
227
|
+
await client.revokeAddress(token, 'alice')
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`username` is optional — omit it and the server allocates one, subject to the domain's policy. Some domains require an API key to register; pass it as `apiKey` and it is sent as `X-API-Key`. Both `revokeAddress` and `registerArkadeIdentity` take an optional `domain` when the server hosts several.
|
|
231
|
+
|
|
232
|
+
### Offline receive
|
|
233
|
+
|
|
234
|
+
Binding an Arkade identity to an address is what lets payments arrive while the wallet is closed — the server takes a solver-mediated swap or a covenant destination on its behalf, and the funds are constrained to pay only the address you register here.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
await client.registerArkadeIdentity({
|
|
238
|
+
token,
|
|
239
|
+
username: 'alice',
|
|
240
|
+
arkadeAddress: 'ark1…',
|
|
241
|
+
claimPublicKey: '02…', // compressed 33-byte key, validated locally
|
|
242
|
+
boardingAddress: 'bc1…', // optional: adds the onchain rail
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Call it again to update it.** The server overwrites, so re-registering is how you point an address at a new Arkade address or claim key; there is no separate update call.
|
|
247
|
+
|
|
248
|
+
`boardingAddress` is the one exception to that overwrite: register it and the address advertises an `onchain` payment option paying it, omit it and an already-registered one is left alone — so a later identity update does not silently withdraw the rail. It is sent unvalidated, unlike `claimPublicKey`, because it is an ordinary Bitcoin address on whatever network the operator runs and this package cannot know which. Payers get no `verify` URL on that rail: nothing server-side observes Bitcoin, so those payments never settle there and a URL could only ever answer "not yet".
|
|
249
|
+
|
|
250
|
+
### With the Arkade SDK
|
|
251
|
+
|
|
252
|
+
The main entry point stays free of `@arkade-os/sdk`, because the payer half of this package has nothing to do with Arkade and a checkout page should not pull the SDK and its Expo peers to ask an address for an invoice. A receiver already has the SDK, so the Arkade-aware helpers live behind a subpath with the SDK as an **optional peer dependency**:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { arkadeIdentityRequest, deriveSessionTokenForIdentity } from '@arkade-os/lnurl-client/arkade'
|
|
256
|
+
|
|
257
|
+
// The key never leaves the wallet, and ECDSA is chosen for you.
|
|
258
|
+
const token = await deriveSessionTokenForIdentity(identity, 'example.com')
|
|
259
|
+
|
|
260
|
+
// Validates the Arkade address and derives claimPublicKey from the identity.
|
|
261
|
+
// boardingAddress is optional here too, and passed through unchecked.
|
|
262
|
+
await client.registerArkadeIdentity(
|
|
263
|
+
await arkadeIdentityRequest({ identity, arkadeAddress, token, username: 'alice' }),
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Three things this buys over doing it by hand:
|
|
268
|
+
|
|
269
|
+
- **`isArkadeAddress` decodes locally.** Without the SDK a malformed address is only caught server-side, a round trip later.
|
|
270
|
+
- **`deriveSessionTokenForIdentity` pins ECDSA.** Schnorr is `Identity.signMessage`'s default and is randomised, which would mint a fresh token on every call and orphan the address — and a schnorr signature is 64 bytes exactly like compact ECDSA, so nothing about the bytes reveals the mistake.
|
|
271
|
+
- **`claimPublicKeyOf` derives the compressed key** in the form the server validates, instead of leaving the caller to extract and hex-encode it.
|
|
272
|
+
|
|
273
|
+
Not yet covered: verifying on-chain that a settled destination payment actually paid the registered address. `StoredPayment` carries `paymentReference` and `covenantScript` for exactly that check, but performing it needs an indexer and is not implemented here — a consumer still trusts the server's word that a payment happened.
|
|
274
|
+
|
|
275
|
+
### Payment activity
|
|
276
|
+
|
|
277
|
+
`listPayments` is a sync source, not just a list: it exists so a wallet that was offline can recover receives it never witnessed, which for the offline rails is every one of them.
|
|
278
|
+
|
|
279
|
+
`syncPayments` drives it for you across every address you hold — paging, cursors, retry and per-server isolation — writing through a store you supply:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { createLnurlClient, syncPayments } from '@arkade-os/lnurl-client'
|
|
283
|
+
|
|
284
|
+
const { synced, failures } = await syncPayments(
|
|
285
|
+
[{ baseUrl: 'https://lnurl.example.com', token, username: 'alice', domain: 'example.com' }],
|
|
286
|
+
{ client: (baseUrl) => createLnurlClient({ baseUrl }), store: myStore },
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**`browserPaymentStore()` is the only storage shipped, and it is `localStorage`** — synchronous, a few megabytes, and it rewrites every record per upsert. Fine for a demo, wrong past that, and absent entirely in Node and React Native. Anything real supplies its own store, the same injection the package uses for `fetchImpl`:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
interface PaymentSyncStore {
|
|
294
|
+
upsert(records: StoredPayment[]): Promise<void> // keyed by record.key; overwrite, never append
|
|
295
|
+
readWatermark(baseUrl: string, lightningAddress: string): Promise<number | undefined>
|
|
296
|
+
writeWatermark(baseUrl: string, lightningAddress: string, since: number): Promise<void> // may move backwards; store verbatim
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`client` is a factory, not a client, because a client is pinned to one `baseUrl`. Sharing one across targets would send every target's bearer token to whichever server that client was built for.
|
|
301
|
+
|
|
302
|
+
A target that fails lands in `failures` while the others keep syncing, and its watermark stays put — so a transient outage cannot silently skip the payments that arrived during it.
|
|
303
|
+
|
|
304
|
+
These matter for correctness. `syncPayments` handles them all; you must handle them yourself if you drive `listPayments` directly:
|
|
305
|
+
|
|
306
|
+
- **`page.source`** carries `{ domain, lightningAddress }`. Store it with each entry: a wallet holding addresses on several servers needs it to attribute them, and the deduplication key is `(baseUrl, identifier)` — identifiers are unique per server, not globally.
|
|
307
|
+
- **`nextSince` is inclusive**, so the boundary row comes back on the next sync. Upsert rather than insert. An exclusive cursor would silently drop payments sharing a millisecond, which is why it works this way.
|
|
308
|
+
- **A creation-ordered cursor cannot track settlement.** `settled` mutates long after a row is created, so a cursor parked at the newest `createdAt` never re-reads an older row that settles later — the wallet shows it pending forever. The server now pages on `updatedAt`, which lists a row again whenever it changes, but older servers page on `createdAt`. For those, `syncPayments` persists the oldest row still waiting to settle instead, and reaches at most one page back so a row nothing will ever settle — an `onchain` destination, which nothing server-side watches, or an invoice nobody paid — costs one extra page per sync rather than re-paging your whole history.
|
|
309
|
+
- **Switch on `kind`, never on the presence of a payment hash.** On the arkade rail the server's own record has no payment hash — the identifier is an opaque verify id — which is why `DestinationActivity` calls it `verifyId` and has no `paymentHash` property at all.
|
|
310
|
+
- **A full page need not advance the cursor.** `nextSince` is the last row's `updatedAt` (`createdAt` on older servers), so if a whole page shares one millisecond the next request returns that same page. Stop and report rather than loop; `syncPayments` fails that target with a terminal `LnurlError`.
|
|
311
|
+
|
|
312
|
+
### Activity resolvers
|
|
313
|
+
|
|
314
|
+
`@arkade-os/lnurl-client/arkade` also exports `lnurlActivityResolver(payments)` and `sentActivityResolver(sends)`, `ActivityResolver`s that label an `ArkTransaction` with the LNURL payment or send it corresponds to. Both take a getter rather than a store: storage — where `payments`/`sends` come from, and where a completed send is recorded (`mergeSentPayment` computes the merge; writing it is yours) — stays entirely with the caller.
|
|
315
|
+
|
|
316
|
+
## Errors
|
|
317
|
+
|
|
318
|
+
Two shapes, both surfaced as `LnurlError`:
|
|
319
|
+
|
|
320
|
+
- LUD-06 endpoints answer HTTP 200 carrying `{ status: "ERROR", reason }`.
|
|
321
|
+
- Management endpoints answer a real status code carrying `{ error, code? }` (for example 409 with `USERNAME_TAKEN`).
|
|
322
|
+
- Rate limits mix the two: HTTP 429 with the `{ status: "ERROR" }` body.
|
|
323
|
+
|
|
324
|
+
`LnurlError` carries `reason`, optional `httpStatus` and `code`, and `retryable`, which is true if and only if `httpStatus === 429`. Everything else is terminal - do not retry it into a different answer.
|
|
325
|
+
|
|
326
|
+
`LnurlTransportError` means the request or the stream itself failed (network down, unreadable SSE body). `LnurlTimeoutError` means `pollVerify` ran past `timeoutMs`; its `lastSnapshot` holds the last verify status seen.
|
|
327
|
+
|
|
328
|
+
## Transport
|
|
329
|
+
|
|
330
|
+
The receiver session is POST-SSE over `fetch`: `openSession` POSTs a JSON body to `/lnurl/session` and reads the event stream from `response.body`.
|
|
331
|
+
|
|
332
|
+
**It is not `EventSource`, and cannot be.** `EventSource` is GET-only and carries neither a request body nor headers, while this protocol needs both — the session token goes in the POST body, and the invoice and settled calls are Bearer-authed. That is a property of the server's endpoint rather than a choice this package makes. The practical cost is that reconnect and backoff are implemented here instead of inherited from the platform.
|
|
333
|
+
|
|
334
|
+
The only requirement is therefore a `fetch` whose `Response` exposes a readable `response.body`. `openSession` throws `LnurlTransportError` immediately when it does not, rather than hanging.
|
|
335
|
+
|
|
336
|
+
### React Native / Expo
|
|
337
|
+
|
|
338
|
+
React Native's global `fetch` does **not** expose a readable `response.body`, so inject [`expo/fetch`](https://docs.expo.dev/versions/latest/sdk/expo/#fetch), which does. This is the same approach `@arkade-os/ts-sdk` takes for its own SSE streams.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { fetch as expoFetch } from 'expo/fetch'
|
|
342
|
+
import { createLnurlClient } from '@arkade-os/lnurl-client'
|
|
343
|
+
|
|
344
|
+
const client = createLnurlClient({
|
|
345
|
+
baseUrl: 'https://lnurl.example.com',
|
|
346
|
+
fetchImpl: expoFetch,
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
No cast is needed: `FetchImpl` is a structural signature — `(input: string, init?: RequestInit) => Promise<Response>` — rather than `typeof globalThis.fetch`, so any conforming implementation is assignable. The same seam takes an instrumented fetch, one that adds headers, or a test double:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const client = createLnurlClient({
|
|
354
|
+
baseUrl: 'https://lnurl.example.com',
|
|
355
|
+
fetchImpl: (url, init) =>
|
|
356
|
+
fetch(url, { ...init, headers: { ...init?.headers, 'X-App': 'wallet' } }),
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Passing nothing resolves `globalThis.fetch` **per call** rather than binding it once, so a client constructed at module scope still picks up a fetch installed later — a mock in a test, a polyfill, or a service worker.
|
|
361
|
+
|
|
362
|
+
The payer surface (`resolve`, `requestInvoice`, `pollVerify`) is plain request/response and works anywhere `fetch` exists, streaming or not.
|
package/dist/arkade.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { PaymentRail, RouteQuote, PaymentRequest, RouterPreferences, PaymentOption, ActivityResolver } from '@arkade-os/sdk';
|
|
2
|
+
import { VerifyExpectation, LnurlClient, DomainCapabilities, PayRequest, PaymentPage, PaymentSyncStore, StoredPayment } from './index.js';
|
|
3
|
+
|
|
4
|
+
declare const LNURL_ARKADE_RAIL = "lnurl-arkade";
|
|
5
|
+
declare const LNURL_LIGHTNING_RAIL = "lnurl-lightning";
|
|
6
|
+
/** What an lnurl rail records on its quote, under `meta.lnurl`. */
|
|
7
|
+
interface LnurlQuoteMeta {
|
|
8
|
+
/** The LNURL or Lightning address the quote pays, as the payer entered it. */
|
|
9
|
+
target: string;
|
|
10
|
+
/** The inner rail that pays it. */
|
|
11
|
+
via: string;
|
|
12
|
+
verify?: string;
|
|
13
|
+
verifyBatch?: string;
|
|
14
|
+
/** What `verify` answers must be about: pass it as `pollVerify`'s `expect`. */
|
|
15
|
+
expect?: VerifyExpectation;
|
|
16
|
+
}
|
|
17
|
+
/** The lnurl meta of a quote, or undefined for a quote no lnurl rail made. */
|
|
18
|
+
declare function lnurlQuoteMeta(quote: RouteQuote): LnurlQuoteMeta | undefined;
|
|
19
|
+
interface LnurlRailDeps {
|
|
20
|
+
/** Payer surface only; needs no `baseUrl`. */
|
|
21
|
+
client: LnurlClient;
|
|
22
|
+
/** Pays the Arkade destination the callback returns — normally `arkRail()`. */
|
|
23
|
+
arkade: PaymentRail;
|
|
24
|
+
/** Pays the BOLT11 the callback returns — normally `solverLightningRail(...)`.
|
|
25
|
+
* Omit it and no lightning rail is registered, rather than one that cannot pay. */
|
|
26
|
+
lightning?: PaymentRail;
|
|
27
|
+
comment?: string;
|
|
28
|
+
/** How long one target's payRequest is reused across `available()`/`quote()`. */
|
|
29
|
+
ttlMs?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Both rails for one client, sharing one resolve.
|
|
33
|
+
*
|
|
34
|
+
* Returned together because the sharing is the point: `options()` calls
|
|
35
|
+
* `available()` on every matching rail, and separate factories would each
|
|
36
|
+
* fetch the same payRequest.
|
|
37
|
+
*/
|
|
38
|
+
declare function lnurlRails(deps: LnurlRailDeps): PaymentRail[];
|
|
39
|
+
|
|
40
|
+
declare function arkadePaymentRouter(opts: {
|
|
41
|
+
wallet: LnurlWallet;
|
|
42
|
+
router?: LnurlPaymentRouter;
|
|
43
|
+
lightning?: PaymentRail;
|
|
44
|
+
client?: LnurlClient;
|
|
45
|
+
comment?: string;
|
|
46
|
+
}): LnurlPaymentRouter;
|
|
47
|
+
|
|
48
|
+
/** The wallet-shaped facade: what a receiver holding an `@arkade-os/sdk` wallet
|
|
49
|
+
* would otherwise wire by hand. The low-level surface stays exported beneath it. */
|
|
50
|
+
|
|
51
|
+
interface LnurlWallet {
|
|
52
|
+
readonly identity: ArkadeSigner;
|
|
53
|
+
getAddress(): Promise<string>;
|
|
54
|
+
getBoardingAddress(): Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
interface LnurlPaymentRouter {
|
|
57
|
+
use(rail: PaymentRail): this;
|
|
58
|
+
remove(id: string): this;
|
|
59
|
+
options(req: PaymentRequest, prefs?: RouterPreferences): Promise<PaymentOption[]>;
|
|
60
|
+
route(req: PaymentRequest, prefs?: RouterPreferences): Promise<RouteQuote>;
|
|
61
|
+
}
|
|
62
|
+
/** The 1023 limit is LUD-01's, not bech32's default 90. */
|
|
63
|
+
declare function encodeLnurl(url: string): string;
|
|
64
|
+
/** A wallet supplies the signer and both addresses. Without one — a script, a
|
|
65
|
+
* test, a server holding only a key — name them; only sending needs a wallet. */
|
|
66
|
+
type ArkadeLnurlSource = {
|
|
67
|
+
wallet: LnurlWallet;
|
|
68
|
+
router?: LnurlPaymentRouter;
|
|
69
|
+
identity?: never;
|
|
70
|
+
arkadeAddress?: never;
|
|
71
|
+
} | {
|
|
72
|
+
wallet?: never;
|
|
73
|
+
router?: never;
|
|
74
|
+
identity: ArkadeSigner;
|
|
75
|
+
arkadeAddress: string;
|
|
76
|
+
boardingAddress?: string;
|
|
77
|
+
};
|
|
78
|
+
type ArkadeLnurlOptions = ArkadeLnurlSource & ArkadeLnurlConfig;
|
|
79
|
+
interface ArkadeLnurlConfig {
|
|
80
|
+
baseUrl: string;
|
|
81
|
+
/** Defaults to `baseUrl`'s hostname, as the server derives it: a LUD-16
|
|
82
|
+
* domain cannot carry a port. */
|
|
83
|
+
domain?: string;
|
|
84
|
+
store?: PaymentSyncStore;
|
|
85
|
+
lightningRail?: PaymentRail;
|
|
86
|
+
client?: LnurlClient;
|
|
87
|
+
}
|
|
88
|
+
/** A claim code is checked only against the reserved name it unlocks, so it never travels alone. */
|
|
89
|
+
type NameOptions = {
|
|
90
|
+
username?: string;
|
|
91
|
+
claimCode?: never;
|
|
92
|
+
} | {
|
|
93
|
+
username: string;
|
|
94
|
+
claimCode: string;
|
|
95
|
+
};
|
|
96
|
+
/** `nameless` is mutually exclusive with `username`/`claimCode` at the type
|
|
97
|
+
* level, matching the server's 400 `invalid_username` on combining them. */
|
|
98
|
+
type ClaimOptions = (NameOptions & {
|
|
99
|
+
nameless?: never;
|
|
100
|
+
}) | {
|
|
101
|
+
nameless: true;
|
|
102
|
+
username?: never;
|
|
103
|
+
claimCode?: never;
|
|
104
|
+
};
|
|
105
|
+
/** One claimed or listed address, holding everything scoped to it. */
|
|
106
|
+
interface Receiver {
|
|
107
|
+
readonly handle: string;
|
|
108
|
+
/** `user@domain`; undefined for a nameless receiver. */
|
|
109
|
+
readonly lightningAddress: string | undefined;
|
|
110
|
+
/** For a QR. The session LNURL when flagged (survives {@link upgrade}), else `.well-known`. */
|
|
111
|
+
readonly lnurl: string;
|
|
112
|
+
/** Addressed through `baseUrl`, not the LUD-16 domain, which drops the port.
|
|
113
|
+
* One fetch answers every call for 30 s, or until a write through this facade
|
|
114
|
+
* (`claim`, `upgrade`); `resolve(lnurl)` always asks the server. */
|
|
115
|
+
payRequest(): Promise<PayRequest>;
|
|
116
|
+
payments(opts?: {
|
|
117
|
+
since?: number;
|
|
118
|
+
limit?: number;
|
|
119
|
+
}): Promise<PaymentPage>;
|
|
120
|
+
sync(): Promise<{
|
|
121
|
+
synced: number;
|
|
122
|
+
failures: unknown[];
|
|
123
|
+
}>;
|
|
124
|
+
/** Names a nameless receiver in place; everything handed out while nameless
|
|
125
|
+
* keeps working. Does not re-bind the Arkade identity, already on the row. */
|
|
126
|
+
upgrade(opts?: NameOptions): Promise<Receiver>;
|
|
127
|
+
}
|
|
128
|
+
interface ArkadeLnurl {
|
|
129
|
+
/** Derived once; signs twice on the first call, nothing on later ones. */
|
|
130
|
+
token(): Promise<string>;
|
|
131
|
+
capabilities(): Promise<DomainCapabilities>;
|
|
132
|
+
/** Registers an address — named, random or nameless per `opts` — and binds
|
|
133
|
+
* the identity. Until the bind lands the address advertises no
|
|
134
|
+
* `paymentOptions` at all, so offline receive does not exist. */
|
|
135
|
+
claim(opts?: ClaimOptions): Promise<Receiver>;
|
|
136
|
+
owned(): Promise<Receiver | undefined>;
|
|
137
|
+
router(): LnurlPaymentRouter;
|
|
138
|
+
options(target: string, amountSat: number, prefs?: RouterPreferences): Promise<PaymentOption[]>;
|
|
139
|
+
}
|
|
140
|
+
/** Ordered so the Arkade leg outranks the Lightning one for the same address:
|
|
141
|
+
* it delivers the full amount with no counterparty. */
|
|
142
|
+
declare const DEFAULT_RAIL_PRIORITY: string[];
|
|
143
|
+
declare function arkadeLnurl(opts: ArkadeLnurlOptions): ArkadeLnurl;
|
|
144
|
+
|
|
145
|
+
/** Namespaced so it cannot clobber the SDK's own resolvers. */
|
|
146
|
+
declare const LNURL_RESOLVER_ID = "lnurl:payments";
|
|
147
|
+
declare const SENT_RESOLVER_ID = "lnurl:sends";
|
|
148
|
+
declare const LNURL_GROUP_PREFIX = "lnurl:";
|
|
149
|
+
declare const SENT_GROUP_PREFIX = "sent:";
|
|
150
|
+
declare const railOf: (p: StoredPayment) => string;
|
|
151
|
+
declare function absorbedPaymentKey(activityId: string): string | undefined;
|
|
152
|
+
/** What a wallet knows about a payment it made. Nothing else does: the server
|
|
153
|
+
* that recorded a send was the recipient's, so its record is theirs, not ours. */
|
|
154
|
+
interface SentPayment {
|
|
155
|
+
/** Arkade txid — the join key to the wallet's own activity row. */
|
|
156
|
+
txid: string;
|
|
157
|
+
/** What the user typed: a Lightning address, an LNURL, or a bare address. */
|
|
158
|
+
target: string;
|
|
159
|
+
railId: string;
|
|
160
|
+
amountSat: number;
|
|
161
|
+
feeSat: number;
|
|
162
|
+
createdAt: number;
|
|
163
|
+
swapId?: string;
|
|
164
|
+
preimage?: string;
|
|
165
|
+
/** The LUD-21 verify URL and LUD-XX verifyBatch endpoint the quote carried, kept so a
|
|
166
|
+
* confirmation still pending when the app closes can be resumed. */
|
|
167
|
+
verify?: string;
|
|
168
|
+
verifyBatch?: string;
|
|
169
|
+
/** Set once the receiver's LUD-21 verify answers, where a rail hands one out. */
|
|
170
|
+
receiverConfirmed?: boolean;
|
|
171
|
+
}
|
|
172
|
+
/** What to keep on file after a completed send: `incoming` replaces the record
|
|
173
|
+
* sharing its txid, so a later verify result updates that row rather than
|
|
174
|
+
* duplicating it. Storage — reading the existing set, writing the result — is
|
|
175
|
+
* the caller's. */
|
|
176
|
+
declare function mergeSentPayment(existing: SentPayment[], incoming: SentPayment): SentPayment[];
|
|
177
|
+
/** Labels a payment this wallet MADE, from what it recorded at send time. Joined
|
|
178
|
+
* on the rail's txid, never inferred from amount and timing as an incoming quote
|
|
179
|
+
* may be: naming a recipient the money did not go to is worse than naming none. */
|
|
180
|
+
declare function sentActivityResolver(sends: () => SentPayment[]): ActivityResolver;
|
|
181
|
+
/** Joined on `payoutReference`, never `paymentReference`: the latter is what the
|
|
182
|
+
* service observed, which on two of the three rails is an output this wallet
|
|
183
|
+
* never holds. */
|
|
184
|
+
declare function lnurlActivityResolver(payments: () => StoredPayment[]): ActivityResolver;
|
|
185
|
+
|
|
186
|
+
/** The slice of `@arkade-os/sdk`'s `Identity` these helpers need. */
|
|
187
|
+
interface ArkadeSigner {
|
|
188
|
+
signMessage(message: Uint8Array, signatureType: "schnorr" | "ecdsa"): Promise<Uint8Array>;
|
|
189
|
+
compressedPublicKey(): Promise<Uint8Array>;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* True when `address` decodes as an Arkade address.
|
|
193
|
+
*
|
|
194
|
+
* The core package deliberately cannot do this — decoding needs the SDK — so
|
|
195
|
+
* without this helper a malformed address is only caught server-side, a round
|
|
196
|
+
* trip later.
|
|
197
|
+
*
|
|
198
|
+
* @param address - Bech32m Arkade address.
|
|
199
|
+
* @returns Whether it decodes.
|
|
200
|
+
*/
|
|
201
|
+
declare function isArkadeAddress(address: string): boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Derives the session token from an `Identity`, signing with ECDSA.
|
|
204
|
+
*
|
|
205
|
+
* Prefer this over calling {@link deriveSessionTokenWithSigner} yourself: it
|
|
206
|
+
* pins the signature type. Schnorr is `Identity.signMessage`'s default and is
|
|
207
|
+
* randomised without an explicit aux, which would mint a fresh token per call
|
|
208
|
+
* and orphan the address — and a schnorr signature is 64 bytes exactly like a
|
|
209
|
+
* compact ECDSA one, so nothing about the bytes reveals the mistake.
|
|
210
|
+
*
|
|
211
|
+
* Produces the same token as `deriveSessionToken(privateKeyHex, domain)`.
|
|
212
|
+
*
|
|
213
|
+
* **Signs twice**, inherited from {@link deriveSessionTokenWithSigner}, to
|
|
214
|
+
* prove the signer is deterministic before a token derived from it is used as
|
|
215
|
+
* an address's identity. Free for a software key; an `Identity` backed by a
|
|
216
|
+
* device that prompts will prompt twice, once per call. Derive once per domain
|
|
217
|
+
* and hold the result for the session rather than calling it per request.
|
|
218
|
+
*
|
|
219
|
+
* @param identity - Anything with `@arkade-os/sdk`'s `signMessage`.
|
|
220
|
+
* @param domain - The LUD-16 domain this token is for.
|
|
221
|
+
* @returns The session token as hex, valid only at that domain.
|
|
222
|
+
*/
|
|
223
|
+
declare function deriveSessionTokenForIdentity(identity: ArkadeSigner, domain: string): Promise<string>;
|
|
224
|
+
/**
|
|
225
|
+
* The identity's compressed public key as hex — the `claimPublicKey` that
|
|
226
|
+
* `registerArkadeIdentity` wants, in the form the server validates.
|
|
227
|
+
*
|
|
228
|
+
* @param identity - Anything with `@arkade-os/sdk`'s `compressedPublicKey`.
|
|
229
|
+
* @returns 66-hex-char compressed key.
|
|
230
|
+
*/
|
|
231
|
+
declare function claimPublicKeyOf(identity: ArkadeSigner): Promise<string>;
|
|
232
|
+
/**
|
|
233
|
+
* Builds the `registerArkadeIdentity` request from an `Identity`, validating the
|
|
234
|
+
* Arkade address before it reaches the wire.
|
|
235
|
+
*
|
|
236
|
+
* An optional `boardingAddress` registers the onchain rail in the same call,
|
|
237
|
+
* unchecked: it is a Bitcoin address on a network this helper cannot know.
|
|
238
|
+
*
|
|
239
|
+
* @param params - The identity, its Arkade address, the owning token and handle.
|
|
240
|
+
* @returns The request body to hand to `registerArkadeIdentity`.
|
|
241
|
+
*/
|
|
242
|
+
declare function arkadeIdentityRequest(params: {
|
|
243
|
+
identity: ArkadeSigner;
|
|
244
|
+
arkadeAddress: string;
|
|
245
|
+
token: string;
|
|
246
|
+
handle: string;
|
|
247
|
+
boardingAddress?: string;
|
|
248
|
+
domain?: string;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
token: string;
|
|
251
|
+
handle: string;
|
|
252
|
+
arkadeAddress: string;
|
|
253
|
+
claimPublicKey: string;
|
|
254
|
+
boardingAddress?: string;
|
|
255
|
+
domain?: string;
|
|
256
|
+
}>;
|
|
257
|
+
|
|
258
|
+
export { type ArkadeLnurl, type ArkadeLnurlConfig, type ArkadeLnurlOptions, type ArkadeLnurlSource, type ArkadeSigner, type ClaimOptions, DEFAULT_RAIL_PRIORITY, LNURL_ARKADE_RAIL, LNURL_GROUP_PREFIX, LNURL_LIGHTNING_RAIL, LNURL_RESOLVER_ID, type LnurlPaymentRouter, type LnurlQuoteMeta, type LnurlRailDeps, type LnurlWallet, type NameOptions, type Receiver, SENT_GROUP_PREFIX, SENT_RESOLVER_ID, type SentPayment, absorbedPaymentKey, arkadeIdentityRequest, arkadeLnurl, arkadePaymentRouter, claimPublicKeyOf, deriveSessionTokenForIdentity, encodeLnurl, isArkadeAddress, lnurlActivityResolver, lnurlQuoteMeta, lnurlRails, mergeSentPayment, railOf, sentActivityResolver };
|