Skip to content

Commit

Permalink
feat(core): allow item placement for all collections
Browse files Browse the repository at this point in the history
  • Loading branch information
capt-nemo429 committed Dec 11, 2022
1 parent c5f0347 commit f24c726
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 64 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/builder/outputBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -86,7 +86,7 @@ export class OutputBuilder {

public addTokens(
tokens: TokenAmount<Amount>[] | TokenAmount<Amount> | TokensCollection,
options?: AddTokenOptions
options?: TokenAddOptions
) {
if (tokens instanceof TokensCollection) {
this._tokens.add(tokens.toArray(), options);
Expand Down
25 changes: 13 additions & 12 deletions packages/core/src/builder/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -117,23 +112,29 @@ export class TransactionBuilder {
return this;
}

public from(inputs: Box<Amount> | Box<Amount>[]): TransactionBuilder {
this._inputs.add(inputs);
public from(
inputs: Box<Amount> | Box<Amount>[],
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<Amount>[] | Box<Amount>): TransactionBuilder {
this._dataInputs.add(dataInputs);
public withDataFrom(
dataInputs: Box<Amount>[] | Box<Amount>,
options?: CollectionAddOptions
): TransactionBuilder {
this._dataInputs.add(dataInputs, options);

return this;
}
Expand Down
65 changes: 61 additions & 4 deletions packages/core/src/models/collections/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ class MockCollection extends Collection<number, number> {
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 {
Expand All @@ -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);
Expand Down
41 changes: 37 additions & 4 deletions packages/core/src/models/collections/collection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { isDefined } from "@fleet-sdk/common";

export type CollectionAddOptions = { index?: number };

export abstract class Collection<InternalType, ExternalType> implements Iterable<InternalType> {
protected readonly _items: InternalType[];

Expand Down Expand Up @@ -38,16 +42,45 @@ export abstract class Collection<InternalType, ExternalType> 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);
}
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/models/collections/inputsCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ export class InputsCollection extends Collection<ErgoUnsignedInput, Box<Amount>>
}
}

protected override _map(input: Box<Amount> | ErgoUnsignedInput): ErgoUnsignedInput {
return input instanceof ErgoUnsignedInput ? input : new ErgoUnsignedInput(input);
}

protected override _addOne(box: Box<Amount>): 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;
Expand Down
27 changes: 2 additions & 25 deletions packages/core/src/models/collections/outputsCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutputBuilder, OutputBuilder> {
constructor(outputs?: OutputBuilder | OutputBuilder[]) {
super();
Expand All @@ -15,29 +13,8 @@ export class OutputsCollection extends Collection<OutputBuilder, 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);
protected _map(output: OutputBuilder): OutputBuilder {
return output;
}

public remove(output: OutputBuilder): number;
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/models/collections/tokensCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
43 changes: 29 additions & 14 deletions packages/core/src/models/collections/tokensCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,65 @@ 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<bigint>, TokenAmount<Amount>> {
constructor();
constructor(token: TokenAmount<Amount>);
constructor(tokens: TokenAmount<Amount>[]);
constructor(tokens: TokenAmount<Amount>[], options: AddTokenOptions);
constructor(tokens?: TokenAmount<Amount> | TokenAmount<Amount>[], options?: AddTokenOptions) {
constructor(tokens: TokenAmount<Amount>[], options: TokenAddOptions);
constructor(tokens?: TokenAmount<Amount> | TokenAmount<Amount>[], options?: TokenAddOptions) {
super();

if (isDefined(tokens)) {
this.add(tokens, options);
}
}

protected override _addOne(token: TokenAmount<Amount>, 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<bigint> | TokenAmount<Amount>): TokenAmount<bigint> {
return { tokenId: token.tokenId, amount: ensureBigInt(token.amount) };
}

return this.length;
}
protected override _addOne(
token: TokenAmount<bigint> | TokenAmount<Amount>,
options?: TokenAddOptions
): number {
if (isUndefined(options) || (options.sum && !isDefined(options.index))) {
if (this._sum(this._map(token))) {
return this.length;
}
}

if (this._items.length >= MAX_TOKENS_PER_BOX) {
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<Amount> | TokenAmount<Amount>[],
options?: AddTokenOptions
options?: TokenAddOptions
): number {
return super._addOneOrMore(items, options);
return super.add(items, options);
}

private _sum(token: TokenAmount<bigint>): 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;
Expand Down

0 comments on commit f24c726

Please sign in to comment.