diff --git a/jest.config.ts b/jest.config.ts index 3d4efc58..0bd19f5b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,6 +3,7 @@ export default { testPathIgnorePatterns: ["/node_modules/", "/dist/", "/coverage/"], collectCoverage: false, testEnvironment: "node", + maxWorkers: 1, // this limit is being used to circumvent this issue https://github.com/facebook/jest/issues/11617 preset: "ts-jest", roots: ["./packages"] }; diff --git a/packages/common/jest.config.ts b/packages/common/jest.config.ts index d34cbbe9..9a96a2d7 100644 --- a/packages/common/jest.config.ts +++ b/packages/common/jest.config.ts @@ -14,7 +14,7 @@ export default { functions: "100" } }, - // maxWorkers: 1, // this limit is being used to circumvent this issue https://github.com/facebook/jest/issues/11617 + maxWorkers: 1, // this limit is being used to circumvent this issue https://github.com/facebook/jest/issues/11617 preset: "ts-jest", roots: ["./src"] }; diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index c2727582..80205d0e 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -14,7 +14,7 @@ export default { functions: "100" } }, - // maxWorkers: 1, // this limit is being used to circumvent this issue https://github.com/facebook/jest/issues/11617 + maxWorkers: 1, // this limit is being used to circumvent this issue https://github.com/facebook/jest/issues/11617 preset: "ts-jest", roots: ["./src"] }; diff --git a/packages/core/src/builder/transactionBuilder.spec.ts b/packages/core/src/builder/transactionBuilder.spec.ts index 2b559d33..f9fa8fd4 100644 --- a/packages/core/src/builder/transactionBuilder.spec.ts +++ b/packages/core/src/builder/transactionBuilder.spec.ts @@ -86,6 +86,27 @@ describe("basic construction", () => { expect(builder.dataInputs).toHaveLength(0); }); + it("Should place outputs at specific index", () => { + const firstOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE, a1.address, height); + const secondOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 2n, a1.address, height); + + const builder = new TransactionBuilder(height) + .from(regularBoxesMock) + .to([firstOutput, secondOutput]); + + expect(builder.outputs.at(0)).toBe(firstOutput); + expect(builder.outputs.at(1)).toBe(secondOutput); + + const placedOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 3n, a2.address, height); + + builder.and.to(placedOutput, { index: 1 }); + expect(builder.outputs.length).toBe(3); + + expect(builder.outputs.at(0)).toBe(firstOutput); // should remain unchanged + expect(builder.outputs.at(1)).toBe(placedOutput); // should be placed at index = 1 + expect(builder.outputs.at(2)).toBe(secondOutput); // should be moved to third place + }); + it("Should set change address by base58 encoded address", () => { const builder = new TransactionBuilder(height).from(regularBoxesMock).sendChangeTo(a1.address); diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index 1176f29c..8d05dc5f 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -22,7 +22,13 @@ import { } from "@fleet-sdk/common"; import { InvalidInput, MalformedTransaction, NotAllowedTokenBurning } from "../errors"; import { NonStandardizedMinting } from "../errors/nonStandardizedMinting"; -import { ErgoAddress, InputsCollection, OutputsCollection, TokensCollection } from "../models"; +import { + AddOutputOptions, + ErgoAddress, + InputsCollection, + OutputsCollection, + TokensCollection +} from "../models"; import { OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; import { BoxSelector } from "./selector"; import { TransactionBuilderSettings } from "./transactionBuilderSettings"; @@ -117,8 +123,11 @@ export class TransactionBuilder { return this; } - public to(outputs: OutputBuilder[] | OutputBuilder): TransactionBuilder { - this._outputs.add(outputs); + public to( + outputs: OutputBuilder[] | OutputBuilder, + options?: AddOutputOptions + ): TransactionBuilder { + this._outputs.add(outputs, options); return this; } diff --git a/packages/core/src/models/collections/collection.spec.ts b/packages/core/src/models/collections/collection.spec.ts index 97ba41d9..56825d0d 100644 --- a/packages/core/src/models/collections/collection.spec.ts +++ b/packages/core/src/models/collections/collection.spec.ts @@ -41,6 +41,24 @@ describe("collection base", () => { expect(collection).toHaveLength(numbers.length); // push on previous line should not affect internal array. }); + it("Should get items by index", () => { + const collection = new MockCollection(); + collection.add(numbers); + + for (let i = 0; i < numbers.length; i++) { + expect(collection.at(i)).toEqual(numbers[i]); + } + }); + + it("Should fail when trying to get item out of index range", () => { + const collection = new MockCollection(); + collection.add(numbers); + + expect(() => { + collection.at(collection.length + 1); + }).toThrow(RangeError); + }); + it("Should iterate correctly for all items", () => { const collection = new MockCollection(); expect(collection.isEmpty).toBeTruthy(); diff --git a/packages/core/src/models/collections/collection.ts b/packages/core/src/models/collections/collection.ts index 89863eb7..a831b476 100644 --- a/packages/core/src/models/collections/collection.ts +++ b/packages/core/src/models/collections/collection.ts @@ -30,6 +30,14 @@ export abstract class Collection implements Iterable return this.length === 0; } + public at(index: number): InternalType { + if (this._isIndexOutOfBounds(index)) { + throw new RangeError(`Index '${index}' is out of range.`); + } + + return this._items[index]; + } + public add(items: ExternalType[] | ExternalType): number { return this._addOneOrMore(items); } diff --git a/packages/core/src/models/collections/outputsCollection.spec.ts b/packages/core/src/models/collections/outputsCollection.spec.ts index b6ef6ecb..bf681266 100644 --- a/packages/core/src/models/collections/outputsCollection.spec.ts +++ b/packages/core/src/models/collections/outputsCollection.spec.ts @@ -31,6 +31,57 @@ describe("outputs collection", () => { expect(first(collection.toArray())).toBe(output); }); + it("Should add a single item at a specific index", () => { + const first = new OutputBuilder(SAFE_MIN_BOX_VALUE, address, height); + const second = new OutputBuilder(SAFE_MIN_BOX_VALUE * 2n, address, height); + + const collection = new OutputsCollection([first, second]); + expect(collection.at(0)).toBe(first); + expect(collection.at(1)).toBe(second); + + const placedOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 3n, address, height); + const newLen = collection.add(placedOutput, { index: 1 }); + expect(newLen).toBe(3); + + expect(collection.at(0)).toBe(first); // should remain unchanged + expect(collection.at(1)).toBe(placedOutput); // should be placed at index = 1 + expect(collection.at(2)).toBe(second); // should be moved to third place + }); + + it("Should append items at a specific index", () => { + const first = new OutputBuilder(SAFE_MIN_BOX_VALUE, address, height); + const second = new OutputBuilder(SAFE_MIN_BOX_VALUE * 2n, address, height); + + const collection = new OutputsCollection([first, second]); + expect(collection.at(0)).toBe(first); + expect(collection.at(1)).toBe(second); + + const fistPlacedOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 3n, address, height); + const secondPlacedOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 4n, address, height); + const newLen = collection.add([fistPlacedOutput, secondPlacedOutput], { index: 1 }); + expect(newLen).toBe(4); + + expect(collection.at(0)).toBe(first); // should remain unchanged + + expect(collection.at(1)).toBe(fistPlacedOutput); // should be placed at index = 1 + expect(collection.at(2)).toBe(secondPlacedOutput); // should be placed at index = 2 + + expect(collection.at(3)).toBe(second); // should be moved to third place + }); + + it("Should fail when trying to add out of range", () => { + const collection = new OutputsCollection([ + new OutputBuilder(SAFE_MIN_BOX_VALUE, address, height), + new OutputBuilder(SAFE_MIN_BOX_VALUE * 2n, address, height) + ]); + + const placedOutput = new OutputBuilder(SAFE_MIN_BOX_VALUE * 3n, address, height); + + expect(() => { + collection.add(placedOutput, { index: 5 /* out of range value */ }); + }).toThrow(RangeError); + }); + it("Should append items", () => { const collection = new OutputsCollection(); diff --git a/packages/core/src/models/collections/outputsCollection.ts b/packages/core/src/models/collections/outputsCollection.ts index b7c61575..5ee3fc99 100644 --- a/packages/core/src/models/collections/outputsCollection.ts +++ b/packages/core/src/models/collections/outputsCollection.ts @@ -4,6 +4,8 @@ import { SelectionTarget } from "../../builder/selector/boxSelector"; import { NotFoundError } from "../../errors"; import { Collection } from "./collection"; +export type AddOutputOptions = { index: number }; + export class OutputsCollection extends Collection { constructor(outputs?: OutputBuilder | OutputBuilder[]) { super(); @@ -13,12 +15,31 @@ export class OutputsCollection extends Collection } } - protected override _addOne(outputBuilder: OutputBuilder): number { - this._items.push(outputBuilder); + protected override _addOne(output: OutputBuilder, options?: AddOutputOptions): number { + if (isDefined(options) && isDefined(options.index)) { + if (this._isIndexOutOfBounds(options.index)) { + throw new RangeError(`Index '${options.index}' is out of range.`); + } + + this._items.splice(options.index, 0, output); + } else { + this._items.push(output); + } return this._items.length; } + public override add( + outputs: OutputBuilder | OutputBuilder[], + options?: AddOutputOptions + ): number { + if (Array.isArray(outputs) && isDefined(options) && isDefined(options.index)) { + return this._addOneOrMore(outputs.reverse(), options); + } + + return this._addOneOrMore(outputs, options); + } + public remove(output: OutputBuilder): number; public remove(index: number): number; public remove(outputs: OutputBuilder | number): number {