lnurlcash-kit 0.3.0 → 0.4.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 +66 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +121 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,72 @@
|
|
|
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.4.0 - 2026-08-26
|
|
7
|
+
|
|
8
|
+
**Breaking: `restoreNotes` asks by hash, and no longer discloses note
|
|
9
|
+
secrets by default.**
|
|
10
|
+
|
|
11
|
+
The walk queries every index up to `gap` past the last one in use, and those
|
|
12
|
+
are exactly the indices the wallet is about to mint into next. Asking by
|
|
13
|
+
secret therefore published the next `gap` secrets the wallet would ever use,
|
|
14
|
+
in cleartext query strings, and the returned `next` then pointed straight at
|
|
15
|
+
one of them. A five-note wallet put twenty-five live-or-future secrets on the
|
|
16
|
+
wire and resumed at an index it had just disclosed.
|
|
17
|
+
|
|
18
|
+
- The walk now uses the informational GET's `h` parameter (LUD-25, "Checking
|
|
19
|
+
a note without exposing it"), so nothing spendable leaves the wallet.
|
|
20
|
+
`fetchNoteInfoByHash` and `buildNoteInfoUrlByHash` expose it directly, and
|
|
21
|
+
`RestoredNote.callback` carries the callback so a caller need not ask again
|
|
22
|
+
with the raw secret.
|
|
23
|
+
- `RestoreOptions.allowSecretDisclosure` (default `false`) permits the old
|
|
24
|
+
form as an explicit fallback, never automatically. When it is used,
|
|
25
|
+
`RestoreResult.disclosesSecrets` is `true` and `next` skips every index the
|
|
26
|
+
walk touched, because a disclosed secret is spent whether or not a note was
|
|
27
|
+
ever minted under it.
|
|
28
|
+
- `h` support is OPTIONAL in LUD-25 with no capability flag, so a SERVICE
|
|
29
|
+
that cannot answer by hash is indistinguishable from one holding none of
|
|
30
|
+
the notes asked about. A walk that never gets a positive answer now throws
|
|
31
|
+
`HashLookupUnsupportedError` rather than reporting an empty wallet.
|
|
32
|
+
`RestoreOptions.probeK1` supplies a positive control, and
|
|
33
|
+
`RestoreResult.hashLookupsConfirmed` reports what was established.
|
|
34
|
+
|
|
35
|
+
**`requestInvoice` names the mint output with a LUD-12 `comment`.**
|
|
36
|
+
|
|
37
|
+
LUD-25 specifies the mint-time output hash as `comment = hex(h)`, and that is
|
|
38
|
+
what a conforming SERVICE reads. The kit sent only `h`, a parameter one
|
|
39
|
+
implementation adopted before the comment form was written, so a wallet
|
|
40
|
+
naming an output against a conforming mint was silently ignored and its note
|
|
41
|
+
was keyed by the payment preimage instead.
|
|
42
|
+
|
|
43
|
+
- `comment` is now sent alongside `h`. A SERVICE reading either gets the same
|
|
44
|
+
hash; one reading neither behaves exactly as before.
|
|
45
|
+
- This matters beyond conformance: an unnamed mint's `k1` **is** the payment
|
|
46
|
+
preimage `P`, so a SERVICE offering LUD-21 `verify` on that payment hands
|
|
47
|
+
the note to whoever holds the verify URL. Naming the output is what makes
|
|
48
|
+
`verify` safe to offer at all.
|
|
49
|
+
|
|
50
|
+
- `PayRequestInfo.commentAllowed` is surfaced and `namesMintOutput()` decides
|
|
51
|
+
the capability from either spelling: LUD-25 advertises it as
|
|
52
|
+
`commentAllowed >= 64`, one mint shipped `mintToHash` first. One rule in
|
|
53
|
+
one place, because both directions of a wrong answer cost a note - read it
|
|
54
|
+
as no and the note is the payment preimage, published on the mint's verify
|
|
55
|
+
URL; read it as yes and the wallet waits for a note minted elsewhere.
|
|
56
|
+
|
|
57
|
+
**Also breaking: an unrecognised refusal no longer aborts a restore, and no
|
|
58
|
+
longer counts toward the gap.**
|
|
59
|
+
|
|
60
|
+
`classifyNoteError` falls through to a bare `ServiceRejectedError` for any
|
|
61
|
+
reason string it has no pattern for, and the walk rethrew it, so a single
|
|
62
|
+
unfamiliar reason from a SERVICE ended the whole restore. Any new note state
|
|
63
|
+
- expiry being the obvious one - would have done it.
|
|
64
|
+
|
|
65
|
+
- Only `NoteUnknownError` advances the gap counter now. Every other refusal
|
|
66
|
+
means the SERVICE knows the index, so it resets the counter and is reported
|
|
67
|
+
in the new `RestoreResult.unresolved`. Advancing it was how a walk could
|
|
68
|
+
terminate early and silently abandon live notes beyond the run.
|
|
69
|
+
- Transport and protocol failures still throw: a SERVICE being down is not a
|
|
70
|
+
statement about an index.
|
|
71
|
+
|
|
6
72
|
## 0.3.0 - 2026-08-24
|
|
7
73
|
|
|
8
74
|
- 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;
|
|
@@ -126,8 +130,13 @@ type PayRequestInfo = {
|
|
|
126
130
|
mintPubkey?: string;
|
|
127
131
|
mintFee?: MintFee;
|
|
128
132
|
mintToHash?: boolean;
|
|
133
|
+
commentAllowed?: number;
|
|
129
134
|
};
|
|
130
135
|
declare const fetchPayRequest: (url: string, options?: LnurlcashOptions) => Promise<PayRequestInfo>;
|
|
136
|
+
declare const namesMintOutput: (info: {
|
|
137
|
+
mintToHash?: boolean;
|
|
138
|
+
commentAllowed?: number;
|
|
139
|
+
}) => boolean;
|
|
131
140
|
type InvoiceResult = {
|
|
132
141
|
pr: string;
|
|
133
142
|
verify?: string;
|
|
@@ -180,16 +189,27 @@ type RestoredNote = {
|
|
|
180
189
|
k1: string;
|
|
181
190
|
amountMsat: number | null;
|
|
182
191
|
state: 'live' | 'pending';
|
|
192
|
+
callback?: string;
|
|
193
|
+
};
|
|
194
|
+
type UnresolvedIndex = {
|
|
195
|
+
index: number;
|
|
196
|
+
k1: string;
|
|
197
|
+
reason: string;
|
|
183
198
|
};
|
|
184
199
|
type RestoreResult = {
|
|
185
200
|
found: RestoredNote[];
|
|
201
|
+
unresolved: UnresolvedIndex[];
|
|
186
202
|
next: number;
|
|
203
|
+
hashLookupsConfirmed: boolean;
|
|
204
|
+
disclosesSecrets: boolean;
|
|
187
205
|
};
|
|
188
206
|
type RestoreOptions = {
|
|
189
207
|
gap?: number;
|
|
190
208
|
start?: number;
|
|
209
|
+
probeK1?: string;
|
|
210
|
+
allowSecretDisclosure?: boolean;
|
|
191
211
|
};
|
|
192
|
-
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
212
|
+
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start, probeK1, allowSecretDisclosure }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
193
213
|
|
|
194
214
|
declare const isBech32Lnurl: (data: string) => boolean;
|
|
195
215
|
declare const toBech32Lnurl: (url: string) => string;
|
|
@@ -210,6 +230,7 @@ declare const noteDeclaredAmount: (url: string) => number | null;
|
|
|
210
230
|
declare const noteSignature: (url: string) => string | null;
|
|
211
231
|
declare const resolveNoteInput: (value: string) => string | null;
|
|
212
232
|
declare const isValidNoteInput: (value: string) => boolean;
|
|
233
|
+
declare const buildNoteInfoUrlByHash: (withdrawLink: string, h: string) => string;
|
|
213
234
|
declare const buildNoteUrl: (withdrawLink: string, k1: string, amountMsat?: number) => string;
|
|
214
235
|
declare const withNewK1: (url: string, k1: string, amountMsat: number, signature?: string) => string;
|
|
215
236
|
declare const withoutK1: (url: string, amountMsat: number, signature?: string) => string;
|
|
@@ -278,6 +299,8 @@ declare class NoteSpentError extends ServiceRejectedError {
|
|
|
278
299
|
declare class NoteUnknownError extends ServiceRejectedError {
|
|
279
300
|
constructor(reason: string);
|
|
280
301
|
}
|
|
302
|
+
declare class HashLookupUnsupportedError extends LnurlcashError {
|
|
303
|
+
}
|
|
281
304
|
declare class AmbiguousMintError extends LnurlcashError {
|
|
282
305
|
}
|
|
283
306
|
declare class AmbiguousMutationError extends AmbiguousMintError {
|
|
@@ -294,6 +317,7 @@ declare const classifyNoteError: (reason: string) => ServiceRejectedError;
|
|
|
294
317
|
|
|
295
318
|
declare const createClient: (options?: LnurlcashOptions) => {
|
|
296
319
|
fetchNoteInfo: (url: string) => Promise<WithdrawRequestInfo>;
|
|
320
|
+
fetchNoteInfoByHash: (withdrawLink: string, h: string) => Promise<NoteInfoByHash>;
|
|
297
321
|
probeBurnedNote: (url: string) => Promise<"live" | "gone" | "unknown">;
|
|
298
322
|
fetchMintAddress: (url: string) => Promise<MintAddressInfo>;
|
|
299
323
|
meltNote: (callback: string, k1: string, pr: string) => Promise<MeltResult>;
|
|
@@ -313,4 +337,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
313
337
|
};
|
|
314
338
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
315
339
|
|
|
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 };
|
|
340
|
+
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, 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;
|
|
@@ -958,9 +992,11 @@ var fetchPayRequest = async (url, options = {}) => {
|
|
|
958
992
|
return {
|
|
959
993
|
...body,
|
|
960
994
|
mintFee: mintFee ?? void 0,
|
|
961
|
-
mintToHash: asBoolean(body.mintToHash)
|
|
995
|
+
mintToHash: asBoolean(body.mintToHash),
|
|
996
|
+
commentAllowed: asNumber(body.commentAllowed)
|
|
962
997
|
};
|
|
963
998
|
};
|
|
999
|
+
var namesMintOutput = (info) => info.mintToHash === true || typeof info.commentAllowed === "number" && info.commentAllowed >= 64;
|
|
964
1000
|
var asBoundMintCommitment = (value) => {
|
|
965
1001
|
if (!value || typeof value !== "object") return void 0;
|
|
966
1002
|
const raw = value;
|
|
@@ -982,7 +1018,9 @@ var requestInvoice = async (payCallback, amountMsat, options = {}) => {
|
|
|
982
1018
|
"An output hash must be 32 bytes of hex - no invoice was requested."
|
|
983
1019
|
);
|
|
984
1020
|
}
|
|
985
|
-
|
|
1021
|
+
const h = options.h.trim().toLowerCase();
|
|
1022
|
+
cbUrl.searchParams.set("comment", h);
|
|
1023
|
+
cbUrl.searchParams.set("h", h);
|
|
986
1024
|
}
|
|
987
1025
|
const body = await lnurlFetch(cbUrl, resolveOptions(options));
|
|
988
1026
|
if (typeof body?.pr !== "string") {
|
|
@@ -1136,18 +1174,86 @@ var settleNoteForValue = async (noteUrl, { mints, minMsat, requireSignature = fa
|
|
|
1136
1174
|
};
|
|
1137
1175
|
|
|
1138
1176
|
// src/restore.ts
|
|
1139
|
-
var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0 } = {}, options = {}) => {
|
|
1177
|
+
var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0, probeK1, allowSecretDisclosure = false } = {}, options = {}) => {
|
|
1140
1178
|
if (!Number.isSafeInteger(gap) || gap < 1) {
|
|
1141
1179
|
throw new RangeError(`The gap limit must be a positive integer, not ${gap}.`);
|
|
1142
1180
|
}
|
|
1181
|
+
if (!Number.isSafeInteger(start) || start < 0) {
|
|
1182
|
+
throw new RangeError(`The start index must be a non-negative integer, not ${start}.`);
|
|
1183
|
+
}
|
|
1184
|
+
let hashLookupsConfirmed = false;
|
|
1185
|
+
if (probeK1) {
|
|
1186
|
+
try {
|
|
1187
|
+
await fetchNoteInfoByHash(baseUrl, hashK1(probeK1), options);
|
|
1188
|
+
hashLookupsConfirmed = true;
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
if (!(err instanceof ServiceRejectedError)) throw err;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const byHash = await walk(
|
|
1194
|
+
start,
|
|
1195
|
+
gap,
|
|
1196
|
+
async (k1) => {
|
|
1197
|
+
const info = await fetchNoteInfoByHash(baseUrl, hashK1(k1), options);
|
|
1198
|
+
hashLookupsConfirmed = true;
|
|
1199
|
+
return info;
|
|
1200
|
+
},
|
|
1201
|
+
root,
|
|
1202
|
+
host
|
|
1203
|
+
);
|
|
1204
|
+
if (byHash.found.length > 0 || byHash.unresolved.length > 0) hashLookupsConfirmed = true;
|
|
1205
|
+
if (hashLookupsConfirmed) {
|
|
1206
|
+
return {
|
|
1207
|
+
found: byHash.found,
|
|
1208
|
+
unresolved: byHash.unresolved,
|
|
1209
|
+
next: byHash.lastUsed === null ? start : byHash.lastUsed + 1,
|
|
1210
|
+
hashLookupsConfirmed: true,
|
|
1211
|
+
disclosesSecrets: false
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
if (!allowSecretDisclosure) {
|
|
1215
|
+
throw new HashLookupUnsupportedError(
|
|
1216
|
+
"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."
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const bySecret = await walk(
|
|
1220
|
+
start,
|
|
1221
|
+
gap,
|
|
1222
|
+
(k1) => fetchNoteInfo(buildNoteUrl(baseUrl, k1), options),
|
|
1223
|
+
root,
|
|
1224
|
+
host
|
|
1225
|
+
);
|
|
1226
|
+
const walkedThrough = bySecret.highestWalked === null ? start - 1 : bySecret.highestWalked;
|
|
1227
|
+
const used = bySecret.lastUsed === null ? start - 1 : bySecret.lastUsed;
|
|
1228
|
+
return {
|
|
1229
|
+
found: bySecret.found,
|
|
1230
|
+
unresolved: bySecret.unresolved,
|
|
1231
|
+
// Every index this walk touched is burned, whether or not a note was
|
|
1232
|
+
// ever minted under it: its secret is in a log somewhere now, so
|
|
1233
|
+
// minting into it later would be minting a note a stranger can spend.
|
|
1234
|
+
next: Math.max(used, walkedThrough) + 1,
|
|
1235
|
+
hashLookupsConfirmed: false,
|
|
1236
|
+
disclosesSecrets: true
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1239
|
+
var walk = async (start, gap, lookup, root, host) => {
|
|
1143
1240
|
const found = [];
|
|
1241
|
+
const unresolved = [];
|
|
1144
1242
|
let lastUsed = null;
|
|
1243
|
+
let highestWalked = null;
|
|
1145
1244
|
let unknownRun = 0;
|
|
1146
1245
|
for (let index = start; unknownRun < gap; index++) {
|
|
1147
1246
|
const k1 = deriveNoteSecret(root, host, index);
|
|
1247
|
+
highestWalked = index;
|
|
1148
1248
|
try {
|
|
1149
|
-
const info = await
|
|
1150
|
-
found.push({
|
|
1249
|
+
const info = await lookup(k1);
|
|
1250
|
+
found.push({
|
|
1251
|
+
index,
|
|
1252
|
+
k1,
|
|
1253
|
+
amountMsat: info.maxWithdrawable,
|
|
1254
|
+
state: "live",
|
|
1255
|
+
callback: info.callback
|
|
1256
|
+
});
|
|
1151
1257
|
lastUsed = index;
|
|
1152
1258
|
unknownRun = 0;
|
|
1153
1259
|
} catch (err) {
|
|
@@ -1160,17 +1266,22 @@ var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0 } = {}, opt
|
|
|
1160
1266
|
unknownRun = 0;
|
|
1161
1267
|
} else if (err instanceof NoteUnknownError) {
|
|
1162
1268
|
unknownRun++;
|
|
1269
|
+
} else if (err instanceof ServiceRejectedError) {
|
|
1270
|
+
unresolved.push({ index, k1, reason: err.reason });
|
|
1271
|
+
lastUsed = index;
|
|
1272
|
+
unknownRun = 0;
|
|
1163
1273
|
} else {
|
|
1164
1274
|
throw err;
|
|
1165
1275
|
}
|
|
1166
1276
|
}
|
|
1167
1277
|
}
|
|
1168
|
-
return { found,
|
|
1278
|
+
return { found, unresolved, lastUsed, highestWalked };
|
|
1169
1279
|
};
|
|
1170
1280
|
|
|
1171
1281
|
// src/index.ts
|
|
1172
1282
|
var createClient = (options = {}) => ({
|
|
1173
1283
|
fetchNoteInfo: (url) => fetchNoteInfo(url, options),
|
|
1284
|
+
fetchNoteInfoByHash: (withdrawLink, h) => fetchNoteInfoByHash(withdrawLink, h, options),
|
|
1174
1285
|
probeBurnedNote: (url) => probeBurnedNote(url, options),
|
|
1175
1286
|
fetchMintAddress: (url) => fetchMintAddress(url, options),
|
|
1176
1287
|
meltNote: (callback, k1, pr) => meltNote(callback, k1, pr, options),
|
|
@@ -1192,4 +1303,4 @@ var createClient = (options = {}) => ({
|
|
|
1192
1303
|
settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
|
|
1193
1304
|
});
|
|
1194
1305
|
|
|
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 };
|
|
1306
|
+
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, 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