lnurlcash-kit 0.4.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 CHANGED
@@ -3,6 +3,39 @@
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
+
6
39
  ## 0.4.0 - 2026-08-26
7
40
 
8
41
  **Breaking: `restoreNotes` asks by hash, and no longer discloses note
package/dist/index.d.ts CHANGED
@@ -112,6 +112,11 @@ type SplitResult = {
112
112
  changeSignature?: string;
113
113
  };
114
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[][];
115
120
  declare const mergeNotes: (callback: string, k1s: string[], options?: LnurlcashOptions) => Promise<RotateResult>;
116
121
  type SettledNote = {
117
122
  k1: string;
@@ -337,4 +342,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
337
342
  };
338
343
  type LnurlcashClient = ReturnType<typeof createClient>;
339
344
 
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 };
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
@@ -948,8 +948,39 @@ var splitNote = async (callback, k1s, amountMsat, options = {}) => {
948
948
  throw keepingOutputs(err, [newK1, changeK1]);
949
949
  }
950
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
+ };
951
980
  var mergeNotes = async (callback, k1s, options = {}) => {
952
981
  const opts = resolveOptions(options);
982
+ const batches = mergeBatches(callback, k1s);
983
+ if (batches.length > 1) return foldNotes(callback, batches, opts, options);
953
984
  const newK1 = opts.randomSecret();
954
985
  try {
955
986
  const result = await mergeNotesWithHash(callback, k1s, hashK1(newK1), options);
@@ -961,6 +992,34 @@ var mergeNotes = async (callback, k1s, options = {}) => {
961
992
  throw keepingOutputs(err, [newK1]);
962
993
  }
963
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
+ };
964
1023
  var settleNote = async (baseUrl, k1, expectedAmountMsat, signature, options = {}) => {
965
1024
  const info = await fetchNoteInfo(
966
1025
  withNewK1(baseUrl, k1, expectedAmountMsat, signature),
@@ -1303,4 +1362,4 @@ var createClient = (options = {}) => ({
1303
1362
  settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
1304
1363
  });
1305
1364
 
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 };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",