lnurlcash-kit 0.3.0 → 0.5.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/CHANGELOG.md +99 -0
- package/dist/index.d.ts +31 -2
- package/dist/index.js +180 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,105 @@
|
|
|
3
3
|
Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
4
4
|
may carry breaking changes; pin an exact version.
|
|
5
5
|
|
|
6
|
+
## 0.5.0 - 2026-08-29
|
|
7
|
+
|
|
8
|
+
**A large merge is folded in batches rather than sent as one over-long
|
|
9
|
+
URL.** LUD-25 bounds a merge by ordinary URL length, not by anything in the
|
|
10
|
+
protocol: every repeated `k1=` costs about 68 characters, and browsers,
|
|
11
|
+
servers and proxies commonly cap a whole URL near 2000. Past roughly 28
|
|
12
|
+
notes `mergeNotes` built a request that something upstream truncates,
|
|
13
|
+
turning a large merge into a malformed one rather than a clean refusal. It
|
|
14
|
+
now measures the URL this SERVICE's own callback actually produces and
|
|
15
|
+
folds the inputs batch into batch, each merge's output carried into the
|
|
16
|
+
next, as the draft advises.
|
|
17
|
+
|
|
18
|
+
A fold can fail with value already moved, which a single merge never could:
|
|
19
|
+
once a batch has landed, the carried note is one the SERVICE has already
|
|
20
|
+
minted, worth every batch folded so far, and the fold holds its only copy.
|
|
21
|
+
It is returned with the failure whatever the class of failure - a network
|
|
22
|
+
drop, a policy refusal, a k1 count the mint caps at, an input mid-melt
|
|
23
|
+
elsewhere - so a caller that persists what `newSecretsOf` hands it cannot
|
|
24
|
+
lose the fold's own output. The error's class is preserved, so a caller
|
|
25
|
+
telling pending apart from spent still can.
|
|
26
|
+
|
|
27
|
+
Batches are bounded by note count as well as URL length. No LUD-25 field
|
|
28
|
+
advertises a SERVICE's own limit on how many `k1` one request may name, and
|
|
29
|
+
the limits in the wild are tighter than 2000 characters allows - moneyer
|
|
30
|
+
defaults to 21, lnurl-mint to 100 - so batching on length alone built
|
|
31
|
+
requests a conforming mint refuses outright. The default is 20, under the
|
|
32
|
+
tightest cap known.
|
|
33
|
+
|
|
34
|
+
`mergeBatches(callback, k1s, options?)` is exported for callers that want to
|
|
35
|
+
plan the batches themselves - `{budget, maxNotes}`, or a bare number for the
|
|
36
|
+
budget as before. A caller that has learned a SERVICE's real limit should
|
|
37
|
+
pass it.
|
|
38
|
+
|
|
39
|
+
## 0.4.0 - 2026-08-26
|
|
40
|
+
|
|
41
|
+
**Breaking: `restoreNotes` asks by hash, and no longer discloses note
|
|
42
|
+
secrets by default.**
|
|
43
|
+
|
|
44
|
+
The walk queries every index up to `gap` past the last one in use, and those
|
|
45
|
+
are exactly the indices the wallet is about to mint into next. Asking by
|
|
46
|
+
secret therefore published the next `gap` secrets the wallet would ever use,
|
|
47
|
+
in cleartext query strings, and the returned `next` then pointed straight at
|
|
48
|
+
one of them. A five-note wallet put twenty-five live-or-future secrets on the
|
|
49
|
+
wire and resumed at an index it had just disclosed.
|
|
50
|
+
|
|
51
|
+
- The walk now uses the informational GET's `h` parameter (LUD-25, "Checking
|
|
52
|
+
a note without exposing it"), so nothing spendable leaves the wallet.
|
|
53
|
+
`fetchNoteInfoByHash` and `buildNoteInfoUrlByHash` expose it directly, and
|
|
54
|
+
`RestoredNote.callback` carries the callback so a caller need not ask again
|
|
55
|
+
with the raw secret.
|
|
56
|
+
- `RestoreOptions.allowSecretDisclosure` (default `false`) permits the old
|
|
57
|
+
form as an explicit fallback, never automatically. When it is used,
|
|
58
|
+
`RestoreResult.disclosesSecrets` is `true` and `next` skips every index the
|
|
59
|
+
walk touched, because a disclosed secret is spent whether or not a note was
|
|
60
|
+
ever minted under it.
|
|
61
|
+
- `h` support is OPTIONAL in LUD-25 with no capability flag, so a SERVICE
|
|
62
|
+
that cannot answer by hash is indistinguishable from one holding none of
|
|
63
|
+
the notes asked about. A walk that never gets a positive answer now throws
|
|
64
|
+
`HashLookupUnsupportedError` rather than reporting an empty wallet.
|
|
65
|
+
`RestoreOptions.probeK1` supplies a positive control, and
|
|
66
|
+
`RestoreResult.hashLookupsConfirmed` reports what was established.
|
|
67
|
+
|
|
68
|
+
**`requestInvoice` names the mint output with a LUD-12 `comment`.**
|
|
69
|
+
|
|
70
|
+
LUD-25 specifies the mint-time output hash as `comment = hex(h)`, and that is
|
|
71
|
+
what a conforming SERVICE reads. The kit sent only `h`, a parameter one
|
|
72
|
+
implementation adopted before the comment form was written, so a wallet
|
|
73
|
+
naming an output against a conforming mint was silently ignored and its note
|
|
74
|
+
was keyed by the payment preimage instead.
|
|
75
|
+
|
|
76
|
+
- `comment` is now sent alongside `h`. A SERVICE reading either gets the same
|
|
77
|
+
hash; one reading neither behaves exactly as before.
|
|
78
|
+
- This matters beyond conformance: an unnamed mint's `k1` **is** the payment
|
|
79
|
+
preimage `P`, so a SERVICE offering LUD-21 `verify` on that payment hands
|
|
80
|
+
the note to whoever holds the verify URL. Naming the output is what makes
|
|
81
|
+
`verify` safe to offer at all.
|
|
82
|
+
|
|
83
|
+
- `PayRequestInfo.commentAllowed` is surfaced and `namesMintOutput()` decides
|
|
84
|
+
the capability from either spelling: LUD-25 advertises it as
|
|
85
|
+
`commentAllowed >= 64`, one mint shipped `mintToHash` first. One rule in
|
|
86
|
+
one place, because both directions of a wrong answer cost a note - read it
|
|
87
|
+
as no and the note is the payment preimage, published on the mint's verify
|
|
88
|
+
URL; read it as yes and the wallet waits for a note minted elsewhere.
|
|
89
|
+
|
|
90
|
+
**Also breaking: an unrecognised refusal no longer aborts a restore, and no
|
|
91
|
+
longer counts toward the gap.**
|
|
92
|
+
|
|
93
|
+
`classifyNoteError` falls through to a bare `ServiceRejectedError` for any
|
|
94
|
+
reason string it has no pattern for, and the walk rethrew it, so a single
|
|
95
|
+
unfamiliar reason from a SERVICE ended the whole restore. Any new note state
|
|
96
|
+
- expiry being the obvious one - would have done it.
|
|
97
|
+
|
|
98
|
+
- Only `NoteUnknownError` advances the gap counter now. Every other refusal
|
|
99
|
+
means the SERVICE knows the index, so it resets the counter and is reported
|
|
100
|
+
in the new `RestoreResult.unresolved`. Advancing it was how a walk could
|
|
101
|
+
terminate early and silently abandon live notes beyond the run.
|
|
102
|
+
- Transport and protocol failures still throw: a SERVICE being down is not a
|
|
103
|
+
statement about an index.
|
|
104
|
+
|
|
6
105
|
## 0.3.0 - 2026-08-24
|
|
7
106
|
|
|
8
107
|
- Additive bound-mint receipt parsing and validation for sealed signers:
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ declare const grossUpForMintFee: (netMsat: number, fee: MintFee) => number;
|
|
|
31
31
|
declare const formatFeePercent: (ppm: number) => string;
|
|
32
32
|
declare const describeMintFee: (fee: MintFee) => string;
|
|
33
33
|
|
|
34
|
+
type NoteInfoByHash = Omit<WithdrawRequestInfo, 'k1'> & {
|
|
35
|
+
k1?: string;
|
|
36
|
+
};
|
|
34
37
|
type WithdrawRequestInfo = {
|
|
35
38
|
tag: 'withdrawRequest';
|
|
36
39
|
callback: string;
|
|
@@ -42,6 +45,7 @@ type WithdrawRequestInfo = {
|
|
|
42
45
|
payLink?: string;
|
|
43
46
|
};
|
|
44
47
|
declare const fetchNoteInfo: (url: string, options?: LnurlcashOptions) => Promise<WithdrawRequestInfo>;
|
|
48
|
+
declare const fetchNoteInfoByHash: (withdrawLink: string, h: string, options?: LnurlcashOptions) => Promise<NoteInfoByHash>;
|
|
45
49
|
declare const probeBurnedNote: (url: string, options?: LnurlcashOptions) => Promise<"live" | "gone" | "unknown">;
|
|
46
50
|
type MintContact = {
|
|
47
51
|
nostr?: string;
|
|
@@ -108,6 +112,11 @@ type SplitResult = {
|
|
|
108
112
|
changeSignature?: string;
|
|
109
113
|
};
|
|
110
114
|
declare const splitNote: (callback: string, k1s: string[], amountMsat: number, options?: LnurlcashOptions) => Promise<SplitResult>;
|
|
115
|
+
type MergeBatchOptions = {
|
|
116
|
+
budget?: number;
|
|
117
|
+
maxNotes?: number;
|
|
118
|
+
};
|
|
119
|
+
declare const mergeBatches: (callback: string, k1s: string[], options?: MergeBatchOptions | number) => string[][];
|
|
111
120
|
declare const mergeNotes: (callback: string, k1s: string[], options?: LnurlcashOptions) => Promise<RotateResult>;
|
|
112
121
|
type SettledNote = {
|
|
113
122
|
k1: string;
|
|
@@ -126,8 +135,13 @@ type PayRequestInfo = {
|
|
|
126
135
|
mintPubkey?: string;
|
|
127
136
|
mintFee?: MintFee;
|
|
128
137
|
mintToHash?: boolean;
|
|
138
|
+
commentAllowed?: number;
|
|
129
139
|
};
|
|
130
140
|
declare const fetchPayRequest: (url: string, options?: LnurlcashOptions) => Promise<PayRequestInfo>;
|
|
141
|
+
declare const namesMintOutput: (info: {
|
|
142
|
+
mintToHash?: boolean;
|
|
143
|
+
commentAllowed?: number;
|
|
144
|
+
}) => boolean;
|
|
131
145
|
type InvoiceResult = {
|
|
132
146
|
pr: string;
|
|
133
147
|
verify?: string;
|
|
@@ -180,16 +194,27 @@ type RestoredNote = {
|
|
|
180
194
|
k1: string;
|
|
181
195
|
amountMsat: number | null;
|
|
182
196
|
state: 'live' | 'pending';
|
|
197
|
+
callback?: string;
|
|
198
|
+
};
|
|
199
|
+
type UnresolvedIndex = {
|
|
200
|
+
index: number;
|
|
201
|
+
k1: string;
|
|
202
|
+
reason: string;
|
|
183
203
|
};
|
|
184
204
|
type RestoreResult = {
|
|
185
205
|
found: RestoredNote[];
|
|
206
|
+
unresolved: UnresolvedIndex[];
|
|
186
207
|
next: number;
|
|
208
|
+
hashLookupsConfirmed: boolean;
|
|
209
|
+
disclosesSecrets: boolean;
|
|
187
210
|
};
|
|
188
211
|
type RestoreOptions = {
|
|
189
212
|
gap?: number;
|
|
190
213
|
start?: number;
|
|
214
|
+
probeK1?: string;
|
|
215
|
+
allowSecretDisclosure?: boolean;
|
|
191
216
|
};
|
|
192
|
-
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
217
|
+
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start, probeK1, allowSecretDisclosure }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
193
218
|
|
|
194
219
|
declare const isBech32Lnurl: (data: string) => boolean;
|
|
195
220
|
declare const toBech32Lnurl: (url: string) => string;
|
|
@@ -210,6 +235,7 @@ declare const noteDeclaredAmount: (url: string) => number | null;
|
|
|
210
235
|
declare const noteSignature: (url: string) => string | null;
|
|
211
236
|
declare const resolveNoteInput: (value: string) => string | null;
|
|
212
237
|
declare const isValidNoteInput: (value: string) => boolean;
|
|
238
|
+
declare const buildNoteInfoUrlByHash: (withdrawLink: string, h: string) => string;
|
|
213
239
|
declare const buildNoteUrl: (withdrawLink: string, k1: string, amountMsat?: number) => string;
|
|
214
240
|
declare const withNewK1: (url: string, k1: string, amountMsat: number, signature?: string) => string;
|
|
215
241
|
declare const withoutK1: (url: string, amountMsat: number, signature?: string) => string;
|
|
@@ -278,6 +304,8 @@ declare class NoteSpentError extends ServiceRejectedError {
|
|
|
278
304
|
declare class NoteUnknownError extends ServiceRejectedError {
|
|
279
305
|
constructor(reason: string);
|
|
280
306
|
}
|
|
307
|
+
declare class HashLookupUnsupportedError extends LnurlcashError {
|
|
308
|
+
}
|
|
281
309
|
declare class AmbiguousMintError extends LnurlcashError {
|
|
282
310
|
}
|
|
283
311
|
declare class AmbiguousMutationError extends AmbiguousMintError {
|
|
@@ -294,6 +322,7 @@ declare const classifyNoteError: (reason: string) => ServiceRejectedError;
|
|
|
294
322
|
|
|
295
323
|
declare const createClient: (options?: LnurlcashOptions) => {
|
|
296
324
|
fetchNoteInfo: (url: string) => Promise<WithdrawRequestInfo>;
|
|
325
|
+
fetchNoteInfoByHash: (withdrawLink: string, h: string) => Promise<NoteInfoByHash>;
|
|
297
326
|
probeBurnedNote: (url: string) => Promise<"live" | "gone" | "unknown">;
|
|
298
327
|
fetchMintAddress: (url: string) => Promise<MintAddressInfo>;
|
|
299
328
|
meltNote: (callback: string, k1: string, pr: string) => Promise<MeltResult>;
|
|
@@ -313,4 +342,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
313
342
|
};
|
|
314
343
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
315
344
|
|
|
316
|
-
export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type DecodeOptions, type HashedMutationResult, type HashedSplitResult, InsufficientValueError, type InvoiceRequestOptions, type InvoiceResult, type LnurlcashClient, LnurlcashError, type LnurlcashOptions, type MeltResult, type MintAddressInfo, type MintClaim, type MintContact, type MintFee, type MintFeeBand, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type ValidatedBoundMintReceipt, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
345
|
+
export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type DecodeOptions, HashLookupUnsupportedError, type HashedMutationResult, type HashedSplitResult, InsufficientValueError, type InvoiceRequestOptions, type InvoiceResult, type LnurlcashClient, LnurlcashError, type LnurlcashOptions, type MeltResult, type MintAddressInfo, type MintClaim, type MintContact, type MintFee, type MintFeeBand, type NoteInfoByHash, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type UnresolvedIndex, type ValidatedBoundMintReceipt, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/dist/index.js
CHANGED
|
@@ -182,6 +182,18 @@ var resolveNoteInput = (value) => {
|
|
|
182
182
|
return url;
|
|
183
183
|
};
|
|
184
184
|
var isValidNoteInput = (value) => resolveNoteInput(value) !== null;
|
|
185
|
+
var buildNoteInfoUrlByHash = (withdrawLink, h) => {
|
|
186
|
+
const hex = h.trim().toLowerCase();
|
|
187
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
188
|
+
throw new Error("A note hash must be 32 bytes of hex.");
|
|
189
|
+
}
|
|
190
|
+
const url = new URL(fromLud17(withdrawLink.trim()));
|
|
191
|
+
url.searchParams.delete("k1");
|
|
192
|
+
url.searchParams.delete("amount");
|
|
193
|
+
url.searchParams.delete("sig");
|
|
194
|
+
url.searchParams.set("h", hex);
|
|
195
|
+
return url.toString();
|
|
196
|
+
};
|
|
185
197
|
var buildNoteUrl = (withdrawLink, k1, amountMsat) => {
|
|
186
198
|
const url = new URL(fromLud17(withdrawLink.trim()));
|
|
187
199
|
url.searchParams.set("k1", k1.trim().toLowerCase());
|
|
@@ -248,6 +260,8 @@ var NoteUnknownError = class extends ServiceRejectedError {
|
|
|
248
260
|
this.message = `The service doesn't recognise this note (service says: "${reason}").`;
|
|
249
261
|
}
|
|
250
262
|
};
|
|
263
|
+
var HashLookupUnsupportedError = class extends LnurlcashError {
|
|
264
|
+
};
|
|
251
265
|
var AmbiguousMintError = class extends LnurlcashError {
|
|
252
266
|
};
|
|
253
267
|
var AmbiguousMutationError = class extends AmbiguousMintError {
|
|
@@ -685,6 +699,11 @@ var verifyNoteSignature = (k1, amountMsat, signatureHex, mintPubkeys) => verifyN
|
|
|
685
699
|
var verifyNoteSignatureHash = (h, amountMsat, signatureHex, mintPubkeys) => verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys).valid;
|
|
686
700
|
|
|
687
701
|
// src/client.ts
|
|
702
|
+
var assertWithdrawRequestShape = (body, { requireK1 }) => {
|
|
703
|
+
if (body?.tag !== "withdrawRequest" || typeof body.callback !== "string" || requireK1 && typeof body.k1 !== "string" || typeof body.maxWithdrawable !== "number" || !Number.isSafeInteger(body.maxWithdrawable) || body.maxWithdrawable < 0 || body.minWithdrawable !== void 0 && (typeof body.minWithdrawable !== "number" || !Number.isSafeInteger(body.minWithdrawable) || body.minWithdrawable < 0 || body.minWithdrawable > body.maxWithdrawable)) {
|
|
704
|
+
throw new ProtocolError("Not a withdrawRequest (unexpected response).");
|
|
705
|
+
}
|
|
706
|
+
};
|
|
688
707
|
var fetchNoteInfo = async (url, options = {}) => {
|
|
689
708
|
const opts = resolveOptions(options);
|
|
690
709
|
const reqUrl = new URL(url);
|
|
@@ -696,9 +715,7 @@ var fetchNoteInfo = async (url, options = {}) => {
|
|
|
696
715
|
if (err instanceof ServiceRejectedError) throw classifyNoteError(err.reason);
|
|
697
716
|
throw err;
|
|
698
717
|
}
|
|
699
|
-
|
|
700
|
-
throw new ProtocolError("Not a withdrawRequest (unexpected response).");
|
|
701
|
-
}
|
|
718
|
+
assertWithdrawRequestShape(body, { requireK1: true });
|
|
702
719
|
const queried = noteK1(url);
|
|
703
720
|
if (queried && body.k1.toLowerCase() !== queried) {
|
|
704
721
|
throw new ProtocolError(
|
|
@@ -711,6 +728,23 @@ var fetchNoteInfo = async (url, options = {}) => {
|
|
|
711
728
|
else info.payLink = payLink;
|
|
712
729
|
return info;
|
|
713
730
|
};
|
|
731
|
+
var fetchNoteInfoByHash = async (withdrawLink, h, options = {}) => {
|
|
732
|
+
const opts = resolveOptions(options);
|
|
733
|
+
const reqUrl = new URL(buildNoteInfoUrlByHash(withdrawLink, h));
|
|
734
|
+
let body;
|
|
735
|
+
try {
|
|
736
|
+
body = await lnurlFetch(reqUrl, opts);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
if (err instanceof ServiceRejectedError) throw classifyNoteError(err.reason);
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
assertWithdrawRequestShape(body, { requireK1: false });
|
|
742
|
+
const info = body;
|
|
743
|
+
const payLink = sameOriginPayLink(body.payLink, reqUrl);
|
|
744
|
+
if (payLink === void 0) delete info.payLink;
|
|
745
|
+
else info.payLink = payLink;
|
|
746
|
+
return info;
|
|
747
|
+
};
|
|
714
748
|
var sameOriginPayLink = (value, noteUrl) => {
|
|
715
749
|
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
716
750
|
let candidate;
|
|
@@ -914,8 +948,39 @@ var splitNote = async (callback, k1s, amountMsat, options = {}) => {
|
|
|
914
948
|
throw keepingOutputs(err, [newK1, changeK1]);
|
|
915
949
|
}
|
|
916
950
|
};
|
|
951
|
+
var MAX_URL_CHARS = 2e3;
|
|
952
|
+
var MAX_NOTES_PER_BATCH = 20;
|
|
953
|
+
var mergeBatches = (callback, k1s, options = {}) => {
|
|
954
|
+
const { budget = MAX_URL_CHARS, maxNotes = MAX_NOTES_PER_BATCH } = typeof options === "number" ? { budget: options } : options;
|
|
955
|
+
const placeholder = "0".repeat(64);
|
|
956
|
+
try {
|
|
957
|
+
new URL(callback);
|
|
958
|
+
} catch {
|
|
959
|
+
throw new RequestRefusedError("The service provided an invalid callback URL.");
|
|
960
|
+
}
|
|
961
|
+
const fits = (candidate, carried) => {
|
|
962
|
+
const url = new URL(callback);
|
|
963
|
+
if (carried) url.searchParams.append("k1", placeholder);
|
|
964
|
+
for (const k1 of candidate) url.searchParams.append("k1", k1);
|
|
965
|
+
url.searchParams.append("h", placeholder);
|
|
966
|
+
return url.href.length <= budget;
|
|
967
|
+
};
|
|
968
|
+
const batches = [];
|
|
969
|
+
let batch = [];
|
|
970
|
+
for (const k1 of k1s) {
|
|
971
|
+
const next = [...batch, k1];
|
|
972
|
+
if (batch.length > 0 && (next.length > maxNotes || !fits(next, batches.length > 0))) {
|
|
973
|
+
batches.push(batch);
|
|
974
|
+
batch = [k1];
|
|
975
|
+
} else batch = next;
|
|
976
|
+
}
|
|
977
|
+
if (batch.length > 0) batches.push(batch);
|
|
978
|
+
return batches;
|
|
979
|
+
};
|
|
917
980
|
var mergeNotes = async (callback, k1s, options = {}) => {
|
|
918
981
|
const opts = resolveOptions(options);
|
|
982
|
+
const batches = mergeBatches(callback, k1s);
|
|
983
|
+
if (batches.length > 1) return foldNotes(callback, batches, opts, options);
|
|
919
984
|
const newK1 = opts.randomSecret();
|
|
920
985
|
try {
|
|
921
986
|
const result = await mergeNotesWithHash(callback, k1s, hashK1(newK1), options);
|
|
@@ -927,6 +992,34 @@ var mergeNotes = async (callback, k1s, options = {}) => {
|
|
|
927
992
|
throw keepingOutputs(err, [newK1]);
|
|
928
993
|
}
|
|
929
994
|
};
|
|
995
|
+
var foldNotes = async (callback, batches, opts, options) => {
|
|
996
|
+
let carried = null;
|
|
997
|
+
let signature;
|
|
998
|
+
for (const batch of batches) {
|
|
999
|
+
const inputs = carried === null ? batch : [carried, ...batch];
|
|
1000
|
+
const newK1 = opts.randomSecret();
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await mergeNotesWithHash(callback, inputs, hashK1(newK1), options);
|
|
1003
|
+
carried = newK1;
|
|
1004
|
+
signature = result.signature;
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
const live = carried === null ? [newK1] : [carried, newK1];
|
|
1007
|
+
if (err instanceof AmbiguousMintError) {
|
|
1008
|
+
throw new AmbiguousMutationError(err.message, live);
|
|
1009
|
+
}
|
|
1010
|
+
if (carried === null) throw keepingOutputs(err, live);
|
|
1011
|
+
if (err instanceof ServiceRejectedError) {
|
|
1012
|
+
err.newSecrets = live;
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
throw new AmbiguousMutationError(
|
|
1016
|
+
err instanceof Error ? err.message : String(err),
|
|
1017
|
+
live
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return { k1: carried, signature };
|
|
1022
|
+
};
|
|
930
1023
|
var settleNote = async (baseUrl, k1, expectedAmountMsat, signature, options = {}) => {
|
|
931
1024
|
const info = await fetchNoteInfo(
|
|
932
1025
|
withNewK1(baseUrl, k1, expectedAmountMsat, signature),
|
|
@@ -958,9 +1051,11 @@ var fetchPayRequest = async (url, options = {}) => {
|
|
|
958
1051
|
return {
|
|
959
1052
|
...body,
|
|
960
1053
|
mintFee: mintFee ?? void 0,
|
|
961
|
-
mintToHash: asBoolean(body.mintToHash)
|
|
1054
|
+
mintToHash: asBoolean(body.mintToHash),
|
|
1055
|
+
commentAllowed: asNumber(body.commentAllowed)
|
|
962
1056
|
};
|
|
963
1057
|
};
|
|
1058
|
+
var namesMintOutput = (info) => info.mintToHash === true || typeof info.commentAllowed === "number" && info.commentAllowed >= 64;
|
|
964
1059
|
var asBoundMintCommitment = (value) => {
|
|
965
1060
|
if (!value || typeof value !== "object") return void 0;
|
|
966
1061
|
const raw = value;
|
|
@@ -982,7 +1077,9 @@ var requestInvoice = async (payCallback, amountMsat, options = {}) => {
|
|
|
982
1077
|
"An output hash must be 32 bytes of hex - no invoice was requested."
|
|
983
1078
|
);
|
|
984
1079
|
}
|
|
985
|
-
|
|
1080
|
+
const h = options.h.trim().toLowerCase();
|
|
1081
|
+
cbUrl.searchParams.set("comment", h);
|
|
1082
|
+
cbUrl.searchParams.set("h", h);
|
|
986
1083
|
}
|
|
987
1084
|
const body = await lnurlFetch(cbUrl, resolveOptions(options));
|
|
988
1085
|
if (typeof body?.pr !== "string") {
|
|
@@ -1136,18 +1233,86 @@ var settleNoteForValue = async (noteUrl, { mints, minMsat, requireSignature = fa
|
|
|
1136
1233
|
};
|
|
1137
1234
|
|
|
1138
1235
|
// src/restore.ts
|
|
1139
|
-
var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0 } = {}, options = {}) => {
|
|
1236
|
+
var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0, probeK1, allowSecretDisclosure = false } = {}, options = {}) => {
|
|
1140
1237
|
if (!Number.isSafeInteger(gap) || gap < 1) {
|
|
1141
1238
|
throw new RangeError(`The gap limit must be a positive integer, not ${gap}.`);
|
|
1142
1239
|
}
|
|
1240
|
+
if (!Number.isSafeInteger(start) || start < 0) {
|
|
1241
|
+
throw new RangeError(`The start index must be a non-negative integer, not ${start}.`);
|
|
1242
|
+
}
|
|
1243
|
+
let hashLookupsConfirmed = false;
|
|
1244
|
+
if (probeK1) {
|
|
1245
|
+
try {
|
|
1246
|
+
await fetchNoteInfoByHash(baseUrl, hashK1(probeK1), options);
|
|
1247
|
+
hashLookupsConfirmed = true;
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
if (!(err instanceof ServiceRejectedError)) throw err;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const byHash = await walk(
|
|
1253
|
+
start,
|
|
1254
|
+
gap,
|
|
1255
|
+
async (k1) => {
|
|
1256
|
+
const info = await fetchNoteInfoByHash(baseUrl, hashK1(k1), options);
|
|
1257
|
+
hashLookupsConfirmed = true;
|
|
1258
|
+
return info;
|
|
1259
|
+
},
|
|
1260
|
+
root,
|
|
1261
|
+
host
|
|
1262
|
+
);
|
|
1263
|
+
if (byHash.found.length > 0 || byHash.unresolved.length > 0) hashLookupsConfirmed = true;
|
|
1264
|
+
if (hashLookupsConfirmed) {
|
|
1265
|
+
return {
|
|
1266
|
+
found: byHash.found,
|
|
1267
|
+
unresolved: byHash.unresolved,
|
|
1268
|
+
next: byHash.lastUsed === null ? start : byHash.lastUsed + 1,
|
|
1269
|
+
hashLookupsConfirmed: true,
|
|
1270
|
+
disclosesSecrets: false
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
if (!allowSecretDisclosure) {
|
|
1274
|
+
throw new HashLookupUnsupportedError(
|
|
1275
|
+
"This service never answered a lookup by hash, so a restore cannot tell an empty wallet from a service that only accepts raw secrets. Pass a probeK1 for a note known to exist here, or allowSecretDisclosure to walk by secret instead."
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
const bySecret = await walk(
|
|
1279
|
+
start,
|
|
1280
|
+
gap,
|
|
1281
|
+
(k1) => fetchNoteInfo(buildNoteUrl(baseUrl, k1), options),
|
|
1282
|
+
root,
|
|
1283
|
+
host
|
|
1284
|
+
);
|
|
1285
|
+
const walkedThrough = bySecret.highestWalked === null ? start - 1 : bySecret.highestWalked;
|
|
1286
|
+
const used = bySecret.lastUsed === null ? start - 1 : bySecret.lastUsed;
|
|
1287
|
+
return {
|
|
1288
|
+
found: bySecret.found,
|
|
1289
|
+
unresolved: bySecret.unresolved,
|
|
1290
|
+
// Every index this walk touched is burned, whether or not a note was
|
|
1291
|
+
// ever minted under it: its secret is in a log somewhere now, so
|
|
1292
|
+
// minting into it later would be minting a note a stranger can spend.
|
|
1293
|
+
next: Math.max(used, walkedThrough) + 1,
|
|
1294
|
+
hashLookupsConfirmed: false,
|
|
1295
|
+
disclosesSecrets: true
|
|
1296
|
+
};
|
|
1297
|
+
};
|
|
1298
|
+
var walk = async (start, gap, lookup, root, host) => {
|
|
1143
1299
|
const found = [];
|
|
1300
|
+
const unresolved = [];
|
|
1144
1301
|
let lastUsed = null;
|
|
1302
|
+
let highestWalked = null;
|
|
1145
1303
|
let unknownRun = 0;
|
|
1146
1304
|
for (let index = start; unknownRun < gap; index++) {
|
|
1147
1305
|
const k1 = deriveNoteSecret(root, host, index);
|
|
1306
|
+
highestWalked = index;
|
|
1148
1307
|
try {
|
|
1149
|
-
const info = await
|
|
1150
|
-
found.push({
|
|
1308
|
+
const info = await lookup(k1);
|
|
1309
|
+
found.push({
|
|
1310
|
+
index,
|
|
1311
|
+
k1,
|
|
1312
|
+
amountMsat: info.maxWithdrawable,
|
|
1313
|
+
state: "live",
|
|
1314
|
+
callback: info.callback
|
|
1315
|
+
});
|
|
1151
1316
|
lastUsed = index;
|
|
1152
1317
|
unknownRun = 0;
|
|
1153
1318
|
} catch (err) {
|
|
@@ -1160,17 +1325,22 @@ var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0 } = {}, opt
|
|
|
1160
1325
|
unknownRun = 0;
|
|
1161
1326
|
} else if (err instanceof NoteUnknownError) {
|
|
1162
1327
|
unknownRun++;
|
|
1328
|
+
} else if (err instanceof ServiceRejectedError) {
|
|
1329
|
+
unresolved.push({ index, k1, reason: err.reason });
|
|
1330
|
+
lastUsed = index;
|
|
1331
|
+
unknownRun = 0;
|
|
1163
1332
|
} else {
|
|
1164
1333
|
throw err;
|
|
1165
1334
|
}
|
|
1166
1335
|
}
|
|
1167
1336
|
}
|
|
1168
|
-
return { found,
|
|
1337
|
+
return { found, unresolved, lastUsed, highestWalked };
|
|
1169
1338
|
};
|
|
1170
1339
|
|
|
1171
1340
|
// src/index.ts
|
|
1172
1341
|
var createClient = (options = {}) => ({
|
|
1173
1342
|
fetchNoteInfo: (url) => fetchNoteInfo(url, options),
|
|
1343
|
+
fetchNoteInfoByHash: (withdrawLink, h) => fetchNoteInfoByHash(withdrawLink, h, options),
|
|
1174
1344
|
probeBurnedNote: (url) => probeBurnedNote(url, options),
|
|
1175
1345
|
fetchMintAddress: (url) => fetchMintAddress(url, options),
|
|
1176
1346
|
meltNote: (callback, k1, pr) => meltNote(callback, k1, pr, options),
|
|
@@ -1192,4 +1362,4 @@ var createClient = (options = {}) => ({
|
|
|
1192
1362
|
settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
|
|
1193
1363
|
});
|
|
1194
1364
|
|
|
1195
|
-
export { AmbiguousMintError, AmbiguousMutationError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
1365
|
+
export { AmbiguousMintError, AmbiguousMutationError, HashLookupUnsupportedError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/package.json
CHANGED