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 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
- if (body?.tag !== "withdrawRequest" || typeof body.callback !== "string" || 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)) {
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
- cbUrl.searchParams.set("h", options.h.trim().toLowerCase());
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 fetchNoteInfo(buildNoteUrl(baseUrl, k1), options);
1150
- found.push({ index, k1, amountMsat: info.maxWithdrawable, state: "live" });
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, next: lastUsed === null ? start : lastUsed + 1 };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "LNURLcash (LUD-25) bearer note client for TypeScript - mint, rotate, split, merge, melt, and verify offline",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",