From 7bd29cc80fd68a3822de27cdea70ed2abd9bac4e Mon Sep 17 00:00:00 2001 From: Kyle Cox Date: Fri, 9 Oct 2026 03:42:24 +0000 Subject: [PATCH] fix(common,core): reject invalid chunk sizes instead of hanging or dropping elements chunk() looped forever for a size of zero or less, returned irregular or empty chunks for a fractional size, silently dropped every element for NaN, and returned the caller's own array as the single chunk when the input fit in one chunk. It now throws a RangeError for a non-positive or non-integer size and always returns copied chunks. The same value reaches chunk() from TransactionBuilderSettings.setMaxTokensPerChangeBox(), which accepted anything: 0 also sends estimateChangeSize's box-count loop to Infinity before chunk() is even reached, so building a transaction with change tokens hung. The setter now throws a RangeError outside 1..MAX_TOKENS_PER_BOX and leaves the previous value unchanged. --- .changeset/tidy-chunks-fail.md | 6 +++++ packages/common/src/utils/array.spec.ts | 23 +++++++++++++++++++ packages/common/src/utils/array.ts | 5 +++- .../transactionBuilderSettings.spec.ts | 20 ++++++++++++++++ .../src/builder/transactionBuilderSettings.ts | 6 +++++ 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-chunks-fail.md diff --git a/.changeset/tidy-chunks-fail.md b/.changeset/tidy-chunks-fail.md new file mode 100644 index 00000000..6aeb6979 --- /dev/null +++ b/.changeset/tidy-chunks-fail.md @@ -0,0 +1,6 @@ +--- +"@fleet-sdk/common": patch +"@fleet-sdk/core": patch +--- + +Fix `chunk()` and `TransactionBuilderSettings.setMaxTokensPerChangeBox()` accepting invalid sizes: a chunk size of zero or less looped forever, a non-integer size produced irregular or empty chunks (NaN silently dropped every element), and a single-chunk result aliased the caller's array. `chunk()` now throws a `RangeError` for a non-positive or non-integer size and always returns copied chunks, and the settings setter throws a `RangeError` for values outside 1..MAX_TOKENS_PER_BOX, leaving the previous value unchanged. diff --git a/packages/common/src/utils/array.spec.ts b/packages/common/src/utils/array.spec.ts index 65c09dfe..4d99ea27 100644 --- a/packages/common/src/utils/array.spec.ts +++ b/packages/common/src/utils/array.spec.ts @@ -162,6 +162,29 @@ describe("Chunk arrays", () => { it("Should return empty array if the input is empty", () => { expect(chunk([], 2)).toEqual([]); }); + + it("Should not return the input array itself as the single chunk", () => { + const array = [1, 2, 3]; + + const chunks = chunk(array, 10); + + expect(chunks).toEqual([[1, 2, 3]]); + expect(chunks[0]).not.toBe(array); + + chunks[0].push(4); + expect(array).toEqual([1, 2, 3]); + }); + + it("Should throw on a non-positive chunk size", () => { + expect(() => chunk([1, 2, 3], 0)).toThrow(RangeError); + expect(() => chunk([1, 2, 3], -2)).toThrow(RangeError); + }); + + it("Should throw on a non-integer chunk size", () => { + expect(() => chunk([1, 2, 3], 2.5)).toThrow(RangeError); + expect(() => chunk([1, 2, 3], Number.NaN)).toThrow(RangeError); + expect(() => chunk([1, 2, 3], Number.POSITIVE_INFINITY)).toThrow(RangeError); + }); }); describe("Array ordering", () => { diff --git a/packages/common/src/utils/array.ts b/packages/common/src/utils/array.ts index 59d9eb28..77a6e460 100644 --- a/packages/common/src/utils/array.ts +++ b/packages/common/src/utils/array.ts @@ -81,8 +81,11 @@ export function hasDuplicatesBy(array: T[], selector: ObjectSelector): boo * ``` */ export function chunk(array: T[], size: number): T[][] { + if (!Number.isInteger(size) || size <= 0) { + throw new RangeError(`Chunk size '${size}' is out of range, it must be a positive integer.`); + } + if (isEmpty(array)) return []; - if (array.length <= size) return [array]; const chunks: T[][] = []; for (let i = 0; i < array.length; i += size) { diff --git a/packages/core/src/builder/transactionBuilderSettings.spec.ts b/packages/core/src/builder/transactionBuilderSettings.spec.ts index 528993e6..d3cc87ab 100644 --- a/packages/core/src/builder/transactionBuilderSettings.spec.ts +++ b/packages/core/src/builder/transactionBuilderSettings.spec.ts @@ -52,6 +52,26 @@ describe("Transaction builder settings", () => { expect(settings.canBurnTokensFromPlugins).toBe(true); }); + it("Should reject invalid max tokens per change box values", () => { + const settings = new TransactionBuilderSettings(); + + for (const invalid of [0, -5, 2.5, Number.NaN, Number.POSITIVE_INFINITY]) { + expect(() => settings.setMaxTokensPerChangeBox(invalid)).toThrow(RangeError); + } + + expect(() => settings.setMaxTokensPerChangeBox(MAX_TOKENS_PER_BOX + 1)).toThrow(RangeError); + expect(settings.maxTokensPerChangeBox).toBe(MAX_TOKENS_PER_BOX); + }); + + it("Should accept boundary max tokens per change box values", () => { + const settings = new TransactionBuilderSettings(); + + expect(settings.setMaxTokensPerChangeBox(1).maxTokensPerChangeBox).toBe(1); + expect(settings.setMaxTokensPerChangeBox(MAX_TOKENS_PER_BOX).maxTokensPerChangeBox).toBe( + MAX_TOKENS_PER_BOX + ); + }); + it("Should allow token burning only from plugins context", () => { const settings = new TransactionBuilderSettings().allowTokenBurningFromPlugins(true); diff --git a/packages/core/src/builder/transactionBuilderSettings.ts b/packages/core/src/builder/transactionBuilderSettings.ts index e846c904..974b50a7 100644 --- a/packages/core/src/builder/transactionBuilderSettings.ts +++ b/packages/core/src/builder/transactionBuilderSettings.ts @@ -33,6 +33,12 @@ export class TransactionBuilderSettings { * Define max number of distinct tokens per change box */ public setMaxTokensPerChangeBox(max: number): TransactionBuilderSettings { + if (!Number.isInteger(max) || max < 1 || max > MAX_TOKENS_PER_BOX) { + throw new RangeError( + `Max tokens per change box '${max}' is out of range, it must be an integer between 1 and ${MAX_TOKENS_PER_BOX}.` + ); + } + this._maxDistinctTokensPerChangeBox = max; return this;