diff --git a/packages/core/src/builder/outputBuilder.ts b/packages/core/src/builder/outputBuilder.ts index 742c6b06..86659b57 100644 --- a/packages/core/src/builder/outputBuilder.ts +++ b/packages/core/src/builder/outputBuilder.ts @@ -23,7 +23,7 @@ import { InvalidRegistersPacking } from "../errors/invalidRegistersPacking"; import { UndefinedCreationHeight } from "../errors/undefinedCreationHeight"; import { UndefinedMintingContext } from "../errors/undefinedMintingContext"; import { ErgoAddress } from "../models"; -import { AddTokenOptions, TokensCollection } from "../models/collections/tokensCollection"; +import { TokenAddOptions, TokensCollection } from "../models/collections/tokensCollection"; import { SConstant } from "../serialization/sigma/constantSerializer"; import { SByte, SColl } from "../serialization/sigma/sigmaTypes"; @@ -86,7 +86,7 @@ export class OutputBuilder { public addTokens( tokens: TokenAmount[] | TokenAmount | TokensCollection, - options?: AddTokenOptions + options?: TokenAddOptions ) { if (tokens instanceof TokensCollection) { this._tokens.add(tokens.toArray(), options); diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index 8d05dc5f..9685971c 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -22,13 +22,8 @@ import { } from "@fleet-sdk/common"; import { InvalidInput, MalformedTransaction, NotAllowedTokenBurning } from "../errors"; import { NonStandardizedMinting } from "../errors/nonStandardizedMinting"; -import { - AddOutputOptions, - ErgoAddress, - InputsCollection, - OutputsCollection, - TokensCollection -} from "../models"; +import { ErgoAddress, InputsCollection, OutputsCollection, TokensCollection } from "../models"; +import { CollectionAddOptions } from "../models/collections/collection"; import { OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; import { BoxSelector } from "./selector"; import { TransactionBuilderSettings } from "./transactionBuilderSettings"; @@ -117,23 +112,29 @@ export class TransactionBuilder { return this; } - public from(inputs: Box | Box[]): TransactionBuilder { - this._inputs.add(inputs); + public from( + inputs: Box | Box[], + options?: CollectionAddOptions + ): TransactionBuilder { + this._inputs.add(inputs, options); return this; } public to( outputs: OutputBuilder[] | OutputBuilder, - options?: AddOutputOptions + options?: CollectionAddOptions ): TransactionBuilder { this._outputs.add(outputs, options); return this; } - public withDataFrom(dataInputs: Box[] | Box): TransactionBuilder { - this._dataInputs.add(dataInputs); + public withDataFrom( + dataInputs: Box[] | Box, + options?: CollectionAddOptions + ): TransactionBuilder { + this._dataInputs.add(dataInputs, options); return this; } diff --git a/packages/core/src/models/collections/collection.spec.ts b/packages/core/src/models/collections/collection.spec.ts index 56825d0d..a5d67c55 100644 --- a/packages/core/src/models/collections/collection.spec.ts +++ b/packages/core/src/models/collections/collection.spec.ts @@ -5,10 +5,8 @@ class MockCollection extends Collection { super(); } - protected override _addOne(numb: number) { - this._items.push(numb); - - return this.length; + protected override _map(item: number): number { + return item; } public remove(item: number): number { @@ -25,6 +23,65 @@ describe("collection base", () => { expect(collection.isEmpty).toBeTruthy(); }); + it("Should add items", () => { + const collection = new MockCollection(); + collection.add(1); + collection.add([2, 3]); + + expect(collection).toHaveLength(3); + expect(collection.at(0)).toBe(1); + expect(collection.at(1)).toBe(2); + expect(collection.at(2)).toBe(3); + }); + + it("Should place one item at a specific index", () => { + const collection = new MockCollection(); + collection.add([1, 2, 3]); + + collection.add(5, { index: 0 }); + + expect(collection).toHaveLength(4); + expect(collection.at(0)).toBe(5); + expect(collection.at(1)).toBe(1); + expect(collection.at(2)).toBe(2); + expect(collection.at(3)).toBe(3); + }); + + it("Should not fail when trying to place at index 0 and collection is empty", () => { + const collection = new MockCollection(); + + collection.add(5, { index: 0 }); + + expect(collection).toHaveLength(1); + expect(collection.at(0)).toBe(5); + }); + + it("Should should fail when trying to add out of range", () => { + const collection = new MockCollection(); + + expect(() => { + collection.add(5, { index: 1 }); + }).toThrow(RangeError); + + expect(() => { + collection.add(5, { index: 2 }); + }).toThrow(RangeError); + }); + + it("Should place multiple items at a specific index", () => { + const collection = new MockCollection(); + collection.add([1, 2, 3]); + + collection.add([10, 20], { index: 2 }); + + expect(collection).toHaveLength(5); + expect(collection.at(0)).toBe(1); + expect(collection.at(1)).toBe(2); + expect(collection.at(2)).toBe(10); + expect(collection.at(3)).toBe(20); + expect(collection.at(4)).toBe(3); + }); + it("Should create a copy of the internal array", () => { const collection = new MockCollection(); collection.add(numbers); diff --git a/packages/core/src/models/collections/collection.ts b/packages/core/src/models/collections/collection.ts index a831b476..991c52f3 100644 --- a/packages/core/src/models/collections/collection.ts +++ b/packages/core/src/models/collections/collection.ts @@ -1,3 +1,7 @@ +import { isDefined } from "@fleet-sdk/common"; + +export type CollectionAddOptions = { index?: number }; + export abstract class Collection implements Iterable { protected readonly _items: InternalType[]; @@ -38,16 +42,45 @@ export abstract class Collection implements Iterable return this._items[index]; } - public add(items: ExternalType[] | ExternalType): number { - return this._addOneOrMore(items); + public add(items: ExternalType[] | ExternalType, options?: CollectionAddOptions): number { + return this._addOneOrMore(items, options); } abstract remove(item: unknown): number; - protected abstract _addOne(item: ExternalType, options?: unknown): number; + protected abstract _map(item: ExternalType | InternalType): InternalType; + + protected _addOne(item: InternalType | ExternalType, options?: CollectionAddOptions): number { + if (isDefined(options) && isDefined(options.index)) { + if (options.index === 0 && this.length === 0) { + this._items.push(this._map(item)); + + return this.length; + } + + if (this._isIndexOutOfBounds(options.index)) { + throw new RangeError(`Index '${options.index}' is out of range.`); + } + + this._items.splice(options.index, 0, this._map(item)); - protected _addOneOrMore(items: ExternalType[] | ExternalType, options?: unknown): number { + return this.length; + } + + this._items.push(this._map(item)); + + return this._items.length; + } + + protected _addOneOrMore( + items: ExternalType[] | ExternalType, + options?: CollectionAddOptions + ): number { if (Array.isArray(items)) { + if (isDefined(options) && isDefined(options.index)) { + items = items.reverse(); + } + for (const item of items) { this._addOne(item, options); } diff --git a/packages/core/src/models/collections/inputsCollection.ts b/packages/core/src/models/collections/inputsCollection.ts index b34bd6b0..eae4ddb4 100644 --- a/packages/core/src/models/collections/inputsCollection.ts +++ b/packages/core/src/models/collections/inputsCollection.ts @@ -16,14 +16,16 @@ export class InputsCollection extends Collection> } } + protected override _map(input: Box | ErgoUnsignedInput): ErgoUnsignedInput { + return input instanceof ErgoUnsignedInput ? input : new ErgoUnsignedInput(input); + } + protected override _addOne(box: Box): number { if (this._items.some((item) => item.boxId === box.boxId)) { throw new DuplicateInputError(box.boxId); } - this._items.push(box instanceof ErgoUnsignedInput ? box : new ErgoUnsignedInput(box)); - - return this._items.length; + return super._addOne(box); } public remove(boxId: BoxId): number; diff --git a/packages/core/src/models/collections/outputsCollection.ts b/packages/core/src/models/collections/outputsCollection.ts index 5ee3fc99..84de4262 100644 --- a/packages/core/src/models/collections/outputsCollection.ts +++ b/packages/core/src/models/collections/outputsCollection.ts @@ -4,8 +4,6 @@ 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(); @@ -15,29 +13,8 @@ export class OutputsCollection extends Collection } } - 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); + protected _map(output: OutputBuilder): OutputBuilder { + return output; } public remove(output: OutputBuilder): number; diff --git a/packages/core/src/models/collections/tokensCollection.spec.ts b/packages/core/src/models/collections/tokensCollection.spec.ts index ab4a06ca..39c9c917 100644 --- a/packages/core/src/models/collections/tokensCollection.spec.ts +++ b/packages/core/src/models/collections/tokensCollection.spec.ts @@ -92,12 +92,40 @@ describe("Tokens collection", () => { expect(collection.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); collection.add({ tokenId: tokenA, amount: 100n }, { sum: true }); + expect(collection).toHaveLength(2); const tokensArray = collection.toArray(); expect(tokensArray.find((x) => x.tokenId === tokenA)?.amount).toEqual(150n); expect(tokensArray.find((x) => x.tokenId === tokenB)?.amount).toEqual(10n); }); + it("Should not sum if the same tokenId is already included but index is set", () => { + const collection = new TokensCollection(); + collection.add({ tokenId: tokenA, amount: 50n }); + collection.add({ tokenId: tokenB, amount: 10n }); + + collection.add({ tokenId: tokenA, amount: 100n }, { sum: true, index: 1 }); + + expect(collection).toHaveLength(3); + expect(collection.at(0).tokenId).toBe(tokenA); + expect(collection.at(1).tokenId).toBe(tokenA); + expect(collection.at(2).tokenId).toBe(tokenB); + }); + + it("Should place token item at specific index", () => { + const collection = new TokensCollection(); + collection.add({ tokenId: tokenA, amount: 50n }); + collection.add({ tokenId: tokenB, amount: 10n }); + + collection.add({ tokenId: tokenB, amount: 100n }, { index: 1 }); + + expect(collection).toHaveLength(3); + + expect(collection.at(0).tokenId).toBe(tokenA); + expect(collection.at(1).tokenId).toBe(tokenB); + expect(collection.at(2).tokenId).toBe(tokenB); + }); + it("Should add if sum = false if tokenId is already included", () => { const collection = new TokensCollection(); collection.add({ tokenId: tokenA, amount: 50n }); diff --git a/packages/core/src/models/collections/tokensCollection.ts b/packages/core/src/models/collections/tokensCollection.ts index 7fe25f8d..016a1d71 100644 --- a/packages/core/src/models/collections/tokensCollection.ts +++ b/packages/core/src/models/collections/tokensCollection.ts @@ -3,18 +3,18 @@ import { ensureBigInt } from "@fleet-sdk/common"; import { NotFoundError } from "../../errors"; import { InsufficientTokenAmount } from "../../errors/insufficientTokenAmount"; import { MaxTokensOverflow } from "../../errors/maxTokensOverflow"; -import { Collection } from "./collection"; +import { Collection, CollectionAddOptions } from "./collection"; export const MAX_TOKENS_PER_BOX = 120; -export type AddTokenOptions = { sum: boolean }; +export type TokenAddOptions = CollectionAddOptions & { sum?: boolean }; export class TokensCollection extends Collection, TokenAmount> { constructor(); constructor(token: TokenAmount); constructor(tokens: TokenAmount[]); - constructor(tokens: TokenAmount[], options: AddTokenOptions); - constructor(tokens?: TokenAmount | TokenAmount[], options?: AddTokenOptions) { + constructor(tokens: TokenAmount[], options: TokenAddOptions); + constructor(tokens?: TokenAmount | TokenAmount[], options?: TokenAddOptions) { super(); if (isDefined(tokens)) { @@ -22,14 +22,17 @@ export class TokensCollection extends Collection, TokenAmoun } } - protected override _addOne(token: TokenAmount, options?: AddTokenOptions): number { - if (!options || isUndefined(options.sum) || options.sum === true) { - for (const t of this._items) { - if (t.tokenId === token.tokenId) { - t.amount += ensureBigInt(token.amount); + protected override _map(token: TokenAmount | TokenAmount): TokenAmount { + return { tokenId: token.tokenId, amount: ensureBigInt(token.amount) }; + } - return this.length; - } + protected override _addOne( + token: TokenAmount | TokenAmount, + options?: TokenAddOptions + ): number { + if (isUndefined(options) || (options.sum && !isDefined(options.index))) { + if (this._sum(this._map(token))) { + return this.length; } } @@ -37,16 +40,28 @@ export class TokensCollection extends Collection, TokenAmoun throw new MaxTokensOverflow(); } - this._items.push({ tokenId: token.tokenId, amount: ensureBigInt(token.amount) }); + super._addOne(token, options); return this.length; } public override add( items: TokenAmount | TokenAmount[], - options?: AddTokenOptions + options?: TokenAddOptions ): number { - return super._addOneOrMore(items, options); + return super.add(items, options); + } + + private _sum(token: TokenAmount): boolean { + for (const t of this._items) { + if (t.tokenId === token.tokenId) { + t.amount += token.amount; + + return true; + } + } + + return false; } public remove(tokenId: TokenId, amount?: Amount): number;