From 03fcb9e3e9bf5dc2404efcd11b81753fd058140c Mon Sep 17 00:00:00 2001 From: capt-nemo429 Date: Thu, 6 Apr 2023 22:13:11 -0300 Subject: [PATCH] feat(core): add min box value estimation --- .../core/src/builder/outputBuilder.spec.ts | 90 +++++++++---- packages/core/src/builder/outputBuilder.ts | 46 +++++-- .../src/builder/transactionBuilder.spec.ts | 68 +++++++++- .../core/src/builder/transactionBuilder.ts | 120 ++++++++++++++---- .../src/models/collections/collection.spec.ts | 7 + .../core/src/models/collections/collection.ts | 12 ++ .../models/collections/outputsCollection.ts | 2 +- .../src/serializer/sigma/boxSerializer.ts | 8 +- packages/core/src/serializer/vlq.ts | 11 +- 9 files changed, 298 insertions(+), 66 deletions(-) diff --git a/packages/core/src/builder/outputBuilder.spec.ts b/packages/core/src/builder/outputBuilder.spec.ts index e240f507..72fb9a71 100644 --- a/packages/core/src/builder/outputBuilder.spec.ts +++ b/packages/core/src/builder/outputBuilder.spec.ts @@ -3,7 +3,7 @@ import { UndefinedCreationHeight } from "../errors/undefinedCreationHeight"; import { UndefinedMintingContext } from "../errors/undefinedMintingContext"; import { ErgoAddress, TokensCollection } from "../models"; import { regularBoxesMock } from "../tests/mocks/mockBoxes"; -import { OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; +import { estimateMinBoxValue, OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; const address = "9fMPy1XY3GW4T6t3LjYofqmzER6x9cV21n5UVJTWmma4Y9mAW6c"; const ergoTree = "0008cd026dc059d64a50d0dbf07755c2c4a4e557e3df8afa7141868b3ab200643d437ee7"; @@ -113,8 +113,8 @@ describe("Token handling", () => { it("Should add distinct tokens", () => { builder.addTokens({ tokenId: tokenA, amount: 50n }).addTokens({ tokenId: tokenB, amount: 10n }); - expect(builder.tokens).toHaveLength(2); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(2); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA)?.amount).toBe(50n); expect(tokens.find((x) => x.tokenId === tokenB)?.amount).toBe(10n); }); @@ -127,8 +127,8 @@ describe("Token handling", () => { ]) ); - expect(builder.tokens).toHaveLength(2); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(2); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); expect(tokens.find((x) => x.tokenId === tokenB)?.amount).toEqual(10n); }); @@ -141,8 +141,8 @@ describe("Token handling", () => { builder.addTokens(collection); - expect(builder.tokens).toHaveLength(2); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(2); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); expect(tokens.find((x) => x.tokenId === tokenB)?.amount).toEqual(10n); }); @@ -151,35 +151,35 @@ describe("Token handling", () => { builder .addTokens({ tokenId: tokenA, amount: "50" }) .addTokens({ tokenId: tokenB, amount: 10n }); - expect(builder.tokens).toHaveLength(2); - expect(builder.tokens.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); + expect(builder.assets).toHaveLength(2); + expect(builder.assets.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); builder.addTokens({ tokenId: tokenA, amount: 100n }); - expect(builder.tokens).toHaveLength(2); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(2); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA)?.amount).toEqual(150n); expect(tokens.find((x) => x.tokenId === tokenB)?.amount).toEqual(10n); }); it("Should add multiple tokens and sum if the same tokenId is added more than one time", () => { builder.addTokens({ tokenId: tokenA, amount: "50" }); - expect(builder.tokens.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); - expect(builder.tokens).toHaveLength(1); + expect(builder.assets.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); + expect(builder.assets).toHaveLength(1); builder.addTokens([ { tokenId: tokenA, amount: 100n }, { tokenId: tokenB, amount: "10" } ]); - expect(builder.tokens).toHaveLength(2); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(2); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA)?.amount).toEqual(150n); expect(tokens.find((x) => x.tokenId === tokenB)?.amount).toEqual(10n); }); it("Should not sum if the same tokenId is added more than one time", () => { builder.addTokens({ tokenId: tokenA, amount: "50" }); - expect(builder.tokens.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); - expect(builder.tokens).toHaveLength(1); + expect(builder.assets.toArray().find((x) => x.tokenId === tokenA)?.amount).toEqual(50n); + expect(builder.assets).toHaveLength(1); builder.addTokens( [ @@ -188,8 +188,8 @@ describe("Token handling", () => { ], { sum: false } ); - expect(builder.tokens).toHaveLength(3); - const tokens = builder.tokens.toArray(); + expect(builder.assets).toHaveLength(3); + const tokens = builder.assets.toArray(); expect(tokens.find((x) => x.tokenId === tokenA && x.amount === 50n)).not.toBeFalsy(); expect(tokens.find((x) => x.tokenId === tokenB && x.amount === 10n)).not.toBeFalsy(); expect(tokens.find((x) => x.tokenId === tokenA && x.amount === 110n)).not.toBeFalsy(); @@ -197,12 +197,12 @@ describe("Token handling", () => { it("Should remove tokens from the list using context ejector", () => { builder.addTokens({ tokenId: tokenA, amount: 50n }).addTokens({ tokenId: tokenB, amount: 10n }); - expect(builder.tokens).toHaveLength(2); + expect(builder.assets).toHaveLength(2); builder.eject(({ tokens }) => tokens.remove(tokenA)); - expect(builder.tokens).toHaveLength(1); - expect(builder.tokens.toArray().find((x) => x.tokenId === tokenA)).toBeFalsy(); + expect(builder.assets).toHaveLength(1); + expect(builder.assets.toArray().find((x) => x.tokenId === tokenA)).toBeFalsy(); }); }); @@ -434,4 +434,50 @@ describe("Building", () => { builder.build(); }).toThrow(UndefinedCreationHeight); }); + + it("Should set min box value", () => { + const output = new OutputBuilder(SAFE_MIN_BOX_VALUE, address); + expect(output.value).toBe(SAFE_MIN_BOX_VALUE); + + output.setValue(SAFE_MIN_BOX_VALUE * 2n); + expect(output.value).toBe(SAFE_MIN_BOX_VALUE * 2n); + }); + + it("Should estimate min box value", () => { + const output = new OutputBuilder(estimateMinBoxValue(), address, height); + expect(output.value).toBe(28440n); + + output.addTokens({ tokenId: tokenA, amount: 10n }); + expect(output.value).toBe(40320n); + + output.addTokens({ tokenId: tokenB, amount: 90n }); + expect(output.value).toBe(52200n); + }); + + it("Should estimate min box value with custom value per byte", () => { + const output = new OutputBuilder(estimateMinBoxValue(200n), address, height); + expect(output.value).toBe(15800n); + + output.addTokens({ tokenId: tokenA, amount: 10n }); + expect(output.value).toBe(22400n); + + output.addTokens({ tokenId: tokenB, amount: 90n }); + expect(output.value).toBe(29000n); + }); + + it("Should replace dynamic value estimation by fixed value", () => { + const output = new OutputBuilder(estimateMinBoxValue(), address, height); + expect(output.value).toBe(28440n); + + output.setValue(SAFE_MIN_BOX_VALUE); + expect(output.value).toBe(SAFE_MIN_BOX_VALUE); + }); + + it("Should replace fixed value by dynamic value estimation", () => { + const output = new OutputBuilder(SAFE_MIN_BOX_VALUE, address, height); + expect(output.value).toBe(SAFE_MIN_BOX_VALUE); + + output.setValue(estimateMinBoxValue()); + expect(output.value).toBe(28440n); + }); }); diff --git a/packages/core/src/builder/outputBuilder.ts b/packages/core/src/builder/outputBuilder.ts index 6f44902a..44676bc8 100644 --- a/packages/core/src/builder/outputBuilder.ts +++ b/packages/core/src/builder/outputBuilder.ts @@ -5,6 +5,7 @@ import { Box, BoxCandidate, ErgoTree, + isDefined, NewToken, NonMandatoryRegisters, OneOrMore, @@ -26,33 +27,41 @@ import { UndefinedCreationHeight } from "../errors/undefinedCreationHeight"; import { UndefinedMintingContext } from "../errors/undefinedMintingContext"; import { ErgoAddress } from "../models"; import { TokenAddOptions, TokensCollection } from "../models/collections/tokensCollection"; +import { estimateBoxSize } from "../serializer/sigma/boxSerializer"; import { SConstant } from "../serializer/sigma/constantSerializer"; import { SByte, SColl } from "../serializer/sigma/sigmaTypes"; +export const BOX_VALUE_PER_BYTE = BigInt(360); export const SAFE_MIN_BOX_VALUE = BigInt(1000000); +export type BoxValueEstimationCallback = (outputBuilder: OutputBuilder) => bigint; + +export function estimateMinBoxValue(valuePerByte = BOX_VALUE_PER_BYTE): BoxValueEstimationCallback { + return (output: OutputBuilder) => { + return BigInt(estimateBoxSize(output, SAFE_MIN_BOX_VALUE)) * valuePerByte; + }; +} + export class OutputBuilder { - private readonly _value: bigint; private readonly _address: ErgoAddress; private readonly _tokens: TokensCollection; + private _value!: bigint; + private _valueEstimator?: BoxValueEstimationCallback; private _creationHeight?: number; private _registers: NonMandatoryRegisters; private _minting?: NewToken; constructor( - value: Amount, + value: Amount | BoxValueEstimationCallback, recipient: Base58String | ErgoTree | ErgoAddress, creationHeight?: number ) { - this._value = ensureBigInt(value); + this.setValue(value); + this._creationHeight = creationHeight; this._tokens = new TokensCollection(); this._registers = {}; - if (this._value <= _0n) { - throw new Error("An UTxO cannot be created without a minimum required amount."); - } - if (typeof recipient === "string") { this._address = isHex(recipient) ? ErgoAddress.fromErgoTree(recipient) @@ -63,7 +72,7 @@ export class OutputBuilder { } public get value(): bigint { - return this._value; + return isDefined(this._valueEstimator) ? this._valueEstimator(this) : this._value; } public get address(): ErgoAddress { @@ -78,7 +87,7 @@ export class OutputBuilder { return this._creationHeight; } - public get tokens(): TokensCollection { + public get assets(): TokensCollection { return this._tokens; } @@ -90,10 +99,25 @@ export class OutputBuilder { return this._minting; } + public setValue(value: Amount | BoxValueEstimationCallback): OutputBuilder { + if (typeof value === "function") { + this._valueEstimator = value; + } else { + this._value = ensureBigInt(value); + this._valueEstimator = undefined; + + if (this._value <= _0n) { + throw new Error("An UTxO cannot be created without a minimum required amount."); + } + } + + return this; + } + public addTokens( tokens: OneOrMore> | TokensCollection, options?: TokenAddOptions - ) { + ): OutputBuilder { if (tokens instanceof TokensCollection) { this._tokens.add(tokens.toArray(), options); } else { @@ -138,7 +162,7 @@ export class OutputBuilder { } public build(transactionInputs?: UnsignedInput[] | Box[]): BoxCandidate { - let tokens = this.tokens.toArray(); + let tokens = this.assets.toArray(); if (this.minting) { if (isEmpty(transactionInputs)) { diff --git a/packages/core/src/builder/transactionBuilder.spec.ts b/packages/core/src/builder/transactionBuilder.spec.ts index d8c72c20..f6cc1e2e 100644 --- a/packages/core/src/builder/transactionBuilder.spec.ts +++ b/packages/core/src/builder/transactionBuilder.spec.ts @@ -1,4 +1,4 @@ -import { Network } from "@fleet-sdk/common"; +import { Amount, BoxCandidate, Network } from "@fleet-sdk/common"; import { ensureBigInt, first, some, sumBy, utxoSum } from "@fleet-sdk/common"; import { stringToBytes } from "@scure/base"; import { InvalidInput } from "../errors"; @@ -7,8 +7,14 @@ import { NonStandardizedMinting } from "../errors/nonStandardizedMinting"; import { NotAllowedTokenBurning } from "../errors/notAllowedTokenBurning"; import { ErgoAddress, ErgoUnsignedInput, MAX_TOKENS_PER_BOX } from "../models"; import { SByte, SColl, SConstant } from "../serializer"; +import { estimateBoxSize } from "../serializer/sigma/boxSerializer"; import { invalidBoxesMock, manyTokensBoxesMock, regularBoxesMock } from "../tests/mocks/mockBoxes"; -import { OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; +import { + BOX_VALUE_PER_BYTE, + estimateMinBoxValue, + OutputBuilder, + SAFE_MIN_BOX_VALUE +} from "./outputBuilder"; import { FEE_CONTRACT, RECOMMENDED_MIN_FEE_VALUE, TransactionBuilder } from "./transactionBuilder"; const height = 844540; @@ -617,6 +623,27 @@ describe("Building", () => { expect(changeOutput.additionalRegisters).toEqual({}); }); + it("Should build with min box value estimation", () => { + const transaction = new TransactionBuilder(height) + .from(manyTokensBoxesMock) + .to( + new OutputBuilder(estimateMinBoxValue(), a2.address) + .addTokens({ + tokenId: "31d6f93435540f52f067efe2c5888b8d4c4418a4fd28156dd834102c8336a804", + amount: 1n + }) + .addTokens({ + tokenId: "8565b6d9b72d0cb8ca052f7e5b8cdf32905333b9e026162e3a6d585ae78e697b", + amount: 1n + }) + ) + .payFee(RECOMMENDED_MIN_FEE_VALUE) + .sendChangeTo(a1.address) + .build(); + + expect(first(transaction.outputs).value).toBe(52200n); + }); + it("Should produce multiple change boxes and run multiple input selections if necessary", () => { const transaction = new TransactionBuilder(height) .from(manyTokensBoxesMock) @@ -664,23 +691,27 @@ describe("Building", () => { expect(change1.ergoTree).toBe(a1.ergoTree); expect(change1.creationHeight).toBe(height); - expect(change1.value).toBe(3465648n); + expect(change1.value).toBe(3595808n); expect(change1.assets).toHaveLength(MAX_TOKENS_PER_BOX); expect(change1.additionalRegisters).toEqual({}); expect(change2.ergoTree).toBe(a1.ergoTree); expect(change2.creationHeight).toBe(height); - expect(change2.value).toBe(SAFE_MIN_BOX_VALUE); + expect(change2.value).toBe(_estimateBoxValue(change2)); expect(change2.assets).toHaveLength(MAX_TOKENS_PER_BOX); expect(change2.additionalRegisters).toEqual({}); expect(change3.ergoTree).toBe(a1.ergoTree); expect(change3.creationHeight).toBe(height); - expect(change3.value).toBe(SAFE_MIN_BOX_VALUE); + expect(change3.value).toBe(_estimateBoxValue(change3)); expect(change3.assets).toHaveLength(32); expect(change3.additionalRegisters).toEqual({}); }); + function _estimateBoxValue(box: BoxCandidate) { + return BigInt(estimateBoxSize(box)) * BOX_VALUE_PER_BYTE; + } + it("Should produce multiple change boxes based on maxTokensPerChangeBox param", () => { const tokensPerBox = 2; @@ -701,6 +732,33 @@ describe("Building", () => { } else { expect(transaction.outputs[i].assets.length <= tokensPerBox).toBeTruthy(); } + + if (i > 0) { + expect(transaction.outputs[i].value).toBe(_estimateBoxValue(transaction.outputs[i])); + } + } + }); + + it("Should produce multiple change boxes based on maxTokensPerChangeBox param with equal tokens", () => { + const tokensPerBox = 3; + + const transaction = new TransactionBuilder(height) + .from(regularBoxesMock) + .sendChangeTo(a1.address) + .configureSelector((selector) => selector.ensureInclusion((i) => some(i.assets))) + .configure((settings) => settings.setMaxTokensPerChangeBox(tokensPerBox)) + .build(); + + expect(transaction.inputs).toHaveLength(4); + expect(transaction.dataInputs).toHaveLength(0); + expect(transaction.outputs).toHaveLength(7); + + for (let i = 0; i < transaction.outputs.length; i++) { + expect(transaction.outputs[i].assets).toHaveLength(tokensPerBox); + + if (i > 0) { + expect(transaction.outputs[i].value).toBe(_estimateBoxValue(transaction.outputs[i])); + } } }); diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index 7862faaf..e4158c0b 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -2,6 +2,8 @@ import { Amount, Base58String, Box, + first, + hexSize, HexString, isUndefined, Network, @@ -15,7 +17,14 @@ import { NonStandardizedMinting } from "../errors/nonStandardizedMinting"; import { ErgoAddress, InputsCollection, OutputsCollection, TokensCollection } from "../models"; import { CollectionAddOptions } from "../models/collections/collection"; import { ErgoUnsignedTransaction } from "../models/ergoUnsignedTransaction"; -import { OutputBuilder, SAFE_MIN_BOX_VALUE } from "./outputBuilder"; +import { BLAKE_256_HASH_LENGTH } from "../serializer/utils"; +import { estimateVLQSize } from "../serializer/vlq"; +import { + BOX_VALUE_PER_BYTE, + estimateMinBoxValue, + OutputBuilder, + SAFE_MIN_BOX_VALUE +} from "./outputBuilder"; import { createPluginContext, FleetPluginContext } from "./pluginContext"; import { BoxSelector } from "./selector"; import { TransactionBuilderSettings } from "./transactionBuilderSettings"; @@ -217,11 +226,14 @@ export class TransactionBuilder { if (this._isTheSameTokenBeingMintedOutsideTheMintingBox()) { throw new NonStandardizedMinting( - "EIP-4 tokens cannot be minted from outside the minting box." + "EIP-4 tokens cannot be minted from outside of the minting box." ); } } + this.outputs + .toArray() + .map((output) => output.setCreationHeight(this._creationHeight, { replace: false })); const outputs = this.outputs.clone(); if (isDefined(this._feeAmount)) { @@ -242,33 +254,55 @@ export class TransactionBuilder { if (isDefined(this._changeAddress)) { let change = utxoSumResultDiff(utxoSum(inputs), target); + const changeBoxes: OutputBuilder[] = []; if (some(change.tokens)) { - let requiredNanoErgs = this._calcRequiredNanoErgsForChange(change.tokens.length); - while (requiredNanoErgs > change.nanoErgs) { + let minRequiredNanoErgs = estimateMinChangeValue({ + changeAddress: this._changeAddress, + creationHeight: this._creationHeight, + tokens: change.tokens, + maxTokensPerBox: this.settings.maxTokensPerChangeBox, + baseIndex: this.outputs.length + 1 + }); + + while (minRequiredNanoErgs > change.nanoErgs) { inputs = selector.select({ - nanoErgs: target.nanoErgs + requiredNanoErgs, + nanoErgs: target.nanoErgs + minRequiredNanoErgs, tokens: target.tokens }); change = utxoSumResultDiff(utxoSum(inputs), target); - requiredNanoErgs = this._calcRequiredNanoErgsForChange(change.tokens.length); + minRequiredNanoErgs = estimateMinChangeValue({ + changeAddress: this._changeAddress, + creationHeight: this._creationHeight, + tokens: change.tokens, + maxTokensPerBox: this.settings.maxTokensPerChangeBox, + baseIndex: this.outputs.length + 1 + }); } const chunkedTokens = chunk(change.tokens, this._settings.maxTokensPerChangeBox); for (const tokens of chunkedTokens) { - const nanoErgs = - change.nanoErgs > requiredNanoErgs - ? change.nanoErgs - requiredNanoErgs + SAFE_MIN_BOX_VALUE - : SAFE_MIN_BOX_VALUE; - change.nanoErgs -= nanoErgs; - - outputs.add(new OutputBuilder(nanoErgs, this._changeAddress).addTokens(tokens)); + const output = new OutputBuilder( + estimateMinBoxValue(), + this._changeAddress, + this._creationHeight + ).addTokens(tokens); + + change.nanoErgs -= output.value; + changeBoxes.push(output); } } if (change.nanoErgs > _0n) { - outputs.add(new OutputBuilder(change.nanoErgs, this._changeAddress)); + if (some(changeBoxes)) { + const firstChangeBox = first(changeBoxes); + firstChangeBox.setValue(firstChangeBox.value + change.nanoErgs); + + outputs.add(changeBoxes); + } else { + outputs.add(new OutputBuilder(change.nanoErgs, this._changeAddress)); + } } } @@ -338,7 +372,7 @@ export class TransactionBuilder { } for (const output of this._outputs) { - if (output.tokens.contains(mintingTokenId)) { + if (output.assets.contains(mintingTokenId)) { return true; } } @@ -357,15 +391,57 @@ export class TransactionBuilder { return tokenId; } +} + +type ChangeEstimationParams = { + changeAddress: ErgoAddress; + creationHeight: number; + tokens: TokenAmount[]; + baseIndex: number; + maxTokensPerBox: number; +}; - private _calcChangeLength(tokensLength: number): number { - return Math.ceil(tokensLength / this._settings.maxTokensPerChangeBox); +function estimateMinChangeValue(params: ChangeEstimationParams): bigint { + const size = BigInt(estimateChangeSize(params)); + + return size * BOX_VALUE_PER_BYTE; +} + +function estimateChangeSize({ + changeAddress, + creationHeight, + tokens, + baseIndex, + maxTokensPerBox +}: ChangeEstimationParams): number { + const neededBoxes = Math.ceil(tokens.length / maxTokensPerBox); + let size = 0; + size += estimateVLQSize(SAFE_MIN_BOX_VALUE); + size += hexSize(changeAddress.ergoTree); + size += estimateVLQSize(creationHeight); + size += estimateVLQSize(0); // empty registers length + size += BLAKE_256_HASH_LENGTH; + + size = size * neededBoxes; + for (let i = 0; i < neededBoxes; i++) { + size += estimateVLQSize(baseIndex + i); } - private _calcRequiredNanoErgsForChange( - tokensLength: number, - minNanoErgsPerBox = SAFE_MIN_BOX_VALUE - ): bigint { - return minNanoErgsPerBox * BigInt(this._calcChangeLength(tokensLength)); + size += tokens.reduce( + (acc: number, curr) => (acc += hexSize(curr.tokenId) + estimateVLQSize(curr.amount)), + 0 + ); + + if (tokens.length > maxTokensPerBox) { + if (tokens.length % maxTokensPerBox > 0) { + size += estimateVLQSize(maxTokensPerBox) * Math.floor(tokens.length / maxTokensPerBox); + size += estimateVLQSize(tokens.length % maxTokensPerBox); + } else { + size += estimateVLQSize(maxTokensPerBox) * neededBoxes; + } + } else { + size += estimateVLQSize(tokens.length); } + + return size; } diff --git a/packages/core/src/models/collections/collection.spec.ts b/packages/core/src/models/collections/collection.spec.ts index 0312c836..07c9ddb3 100644 --- a/packages/core/src/models/collections/collection.spec.ts +++ b/packages/core/src/models/collections/collection.spec.ts @@ -129,4 +129,11 @@ describe("collection base", () => { expect(n).toBe(numbers[counter++]); } }); + + it("Should reduce", () => { + const collection = new MockCollection(); + collection.add([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + expect(collection.reduce((acc, curr) => (acc += curr), 0)).toBe(45); + }); }); diff --git a/packages/core/src/models/collections/collection.ts b/packages/core/src/models/collections/collection.ts index ba94d5d6..47a76e93 100644 --- a/packages/core/src/models/collections/collection.ts +++ b/packages/core/src/models/collections/collection.ts @@ -91,4 +91,16 @@ export abstract class Collection implements Iterable public toArray(): InternalType[] { return [...this._items]; } + + public reduce( + callbackFn: ( + accumulator: U, + currentValue: InternalType, + currentIndex: number, + array: InternalType[] + ) => U, + initialValue: U + ): U { + return this._items.reduce(callbackFn, initialValue); + } } diff --git a/packages/core/src/models/collections/outputsCollection.ts b/packages/core/src/models/collections/outputsCollection.ts index 3a642ee9..e11e061c 100644 --- a/packages/core/src/models/collections/outputsCollection.ts +++ b/packages/core/src/models/collections/outputsCollection.ts @@ -70,7 +70,7 @@ export class OutputsCollection extends Collection for (const box of this._items) { nanoErgs += box.value; - for (const token of box.tokens) { + for (const token of box.assets) { tokens[token.tokenId] = (tokens[token.tokenId] || _0n) + token.amount; } } diff --git a/packages/core/src/serializer/sigma/boxSerializer.ts b/packages/core/src/serializer/sigma/boxSerializer.ts index 65c5d257..e9229e4c 100644 --- a/packages/core/src/serializer/sigma/boxSerializer.ts +++ b/packages/core/src/serializer/sigma/boxSerializer.ts @@ -11,9 +11,12 @@ import { import { ensureBigInt, isDefined, isEmpty } from "@fleet-sdk/common"; import { OutputBuilder } from "../../builder"; import { ErgoBox } from "../../models/ergoBox"; +import { BLAKE_256_HASH_LENGTH } from "../utils"; import { estimateVLQSize } from "../vlq"; import { SigmaWriter } from "./sigmaWriter"; +const MAX_UINT16_VALUE = 65535; + export function serializeBox(box: Box | ErgoBox): SigmaWriter; export function serializeBox(box: Box | ErgoBox, writer: SigmaWriter): SigmaWriter; export function serializeBox( @@ -99,9 +102,6 @@ function writeRegisters(writer: SigmaWriter, registers: NonMandatoryRegisters): } } -const MAX_UINT16_VALUE = 65535; -const TRANSACTION_ID_BYTE_SIZE = 32; - /** * Estimates the byte size a box. * @returns byte size of the box. @@ -141,7 +141,7 @@ export function estimateBoxSize( } size += estimateVLQSize(registersLength); - size += TRANSACTION_ID_BYTE_SIZE; + size += BLAKE_256_HASH_LENGTH; // transaction id size += estimateVLQSize(isBox(box) ? box.index : MAX_UINT16_VALUE); return size; diff --git a/packages/core/src/serializer/vlq.ts b/packages/core/src/serializer/vlq.ts index 5b414b41..869bcba8 100644 --- a/packages/core/src/serializer/vlq.ts +++ b/packages/core/src/serializer/vlq.ts @@ -120,9 +120,18 @@ export function readBigVLQ(reader: SigmaReader): bigint { * @returns the byte size of the value. */ export function estimateVLQSize(value: number | bigint | string): number { - value = ensureBigInt(value); let size = 0; + if (typeof value === "number") { + do { + size++; + value = Math.floor(value / 128); + } while (value > 0); + + return size; + } + + value = ensureBigInt(value); do { size++; value /= _128n;