From 9d8343a088bddc4689f8d94aae90c02456dd4d8e Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:42:36 -0300 Subject: [PATCH 1/8] refactor --- .../core/src/builder/selector/boxSelector.ts | 16 ++---- .../core/src/builder/transactionBuilder.ts | 57 ++++++++++--------- .../models/collections/inputsCollection.ts | 10 +--- .../models/collections/outputsCollection.ts | 5 +- .../models/collections/tokensCollection.ts | 5 +- plugins/babel-fees/src/plugins.ts | 12 +--- 6 files changed, 44 insertions(+), 61 deletions(-) diff --git a/packages/core/src/builder/selector/boxSelector.ts b/packages/core/src/builder/selector/boxSelector.ts index 801f557..0135ce6 100644 --- a/packages/core/src/builder/selector/boxSelector.ts +++ b/packages/core/src/builder/selector/boxSelector.ts @@ -68,11 +68,9 @@ export class BoxSelector> { const inclusion = this._ensureInclusionBoxIds; if (predicate) { - if (inclusion) { - selected = unselected.filter((box) => predicate(box) || inclusion.has(box.boxId)); - } else { - selected = unselected.filter(predicate); - } + selected = inclusion + ? unselected.filter((box) => predicate(box) || inclusion.has(box.boxId)) + : unselected.filter(predicate); } else if (inclusion) { selected = unselected.filter((box) => inclusion.has(box.boxId)); } @@ -88,9 +86,7 @@ export class BoxSelector> { if (some(remaining.tokens) && selected.some((input) => !isEmpty(input.assets))) { for (const t of remaining.tokens) { - if (t.amount && t.amount > _0n) { - t.amount -= utxoSum(selected, t.tokenId); - } + if (t.amount && t.amount > _0n) t.amount -= utxoSum(selected, t.tokenId); } } } @@ -177,9 +173,7 @@ export class BoxSelector> { } if (Array.isArray(predicateOrBoxIds)) { - for (const boxId of predicateOrBoxIds) { - this._ensureInclusionBoxIds.add(boxId); - } + for (const boxId of predicateOrBoxIds) this._ensureInclusionBoxIds.add(boxId); } else { this._ensureInclusionBoxIds.add(predicateOrBoxIds); } diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index a9fc47c..032114d 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -1,21 +1,21 @@ import { - _0n, type Amount, type Base58String, type Box, + type CollectionAddOptions, + type HexString, + type OneOrMore, + type TokenAmount, + _0n, byteSizeOf, chunk, - type CollectionAddOptions, ensureBigInt, first, - type HexString, isDefined, isHex, isUndefined, Network, - type OneOrMore, some, - type TokenAmount, utxoDiff, utxoSum } from "@fleet-sdk/common"; @@ -59,8 +59,20 @@ type EjectorContext = { selection: (selectorCallBack: SelectorCallback) => void; }; +/** + * Options for including inputs in the transaction builder + */ +type InputsInclusionOptions = { + /** + * If true, all the inputs will be included in the + * transaction while preserving the original order. + */ + ensureInclusion?: boolean; +}; + export class TransactionBuilder { private readonly _inputs!: InputsCollection; + private readonly _preservedInputs!: InputsCollection; private readonly _dataInputs!: InputsCollection; private readonly _outputs!: OutputsCollection; private readonly _settings!: TransactionBuilderSettings; @@ -127,9 +139,16 @@ export class TransactionBuilder { } public from( - inputs: OneOrMore> | CollectionLike> + inputs: OneOrMore> | CollectionLike>, + options: InputsInclusionOptions = { ensureInclusion: false } ): TransactionBuilder { - this._inputs.add(isCollectionLike(inputs) ? inputs.toArray() : inputs); + const items = isCollectionLike(inputs) ? inputs.toArray() : inputs; + if (options.ensureInclusion) { + this._preservedInputs.add(items); + } else { + this._inputs.add(items); + } + return this; } @@ -193,19 +212,13 @@ export class TransactionBuilder { } public configureSelector(selectorCallback: SelectorCallback): TransactionBuilder { - if (isUndefined(this._selectorCallbacks)) { - this._selectorCallbacks = []; - } - + if (isUndefined(this._selectorCallbacks)) this._selectorCallbacks = []; this._selectorCallbacks.push(selectorCallback); - return this; } public extend(plugins: FleetPlugin): TransactionBuilder { - if (!this._plugins) { - this._plugins = []; - } + if (!this._plugins) this._plugins = []; this._plugins.push({ execute: plugins, pending: true }); return this; @@ -331,9 +344,7 @@ export class TransactionBuilder { } for (const input of inputs) { - if (!input.isValid()) { - throw new InvalidInput(input.boxId); - } + if (!input.isValid()) throw new InvalidInput(input.boxId); } const unsignedTransaction = new ErgoUnsignedTransaction( @@ -366,10 +377,7 @@ export class TransactionBuilder { } private _isMinting(): boolean { - for (const output of this._outputs) { - if (output.minting) return true; - } - + for (const output of this._outputs) if (output.minting) return true; return false; } @@ -428,7 +436,6 @@ type ChangeEstimationParams = { function estimateMinChangeValue(params: ChangeEstimationParams): bigint { const size = BigInt(estimateChangeSize(params)); - return size * BOX_VALUE_PER_BYTE; } @@ -448,9 +455,7 @@ function estimateChangeSize({ size += 32; // BLAKE 256 hash length size = size * neededBoxes; - for (let i = 0; i < neededBoxes; i++) { - size += estimateVLQSize(baseIndex + i); - } + for (let i = 0; i < neededBoxes; i++) size += estimateVLQSize(baseIndex + i); for (const token of tokens) { size += byteSizeOf(token.tokenId) + estimateVLQSize(token.amount); diff --git a/packages/core/src/models/collections/inputsCollection.ts b/packages/core/src/models/collections/inputsCollection.ts index a7593e9..ab46d69 100644 --- a/packages/core/src/models/collections/inputsCollection.ts +++ b/packages/core/src/models/collections/inputsCollection.ts @@ -2,8 +2,8 @@ import { type Amount, type Box, type BoxId, - Collection, - type OneOrMore + type OneOrMore, + Collection } from "@fleet-sdk/common"; import { isDefined } from "@fleet-sdk/common"; import { DuplicateInputError, NotFoundError } from "../../errors"; @@ -15,10 +15,7 @@ export class InputsCollection extends Collection> constructor(boxes: Box[]); constructor(boxes?: OneOrMore>) { super(); - - if (isDefined(boxes)) { - this.add(boxes); - } + if (isDefined(boxes)) this.add(boxes); } protected override _map(input: Box | ErgoUnsignedInput): ErgoUnsignedInput { @@ -54,7 +51,6 @@ export class InputsCollection extends Collection> } this._items.splice(index, 1); - return this.length; } } diff --git a/packages/core/src/models/collections/outputsCollection.ts b/packages/core/src/models/collections/outputsCollection.ts index 7a2ff53..27ffad4 100644 --- a/packages/core/src/models/collections/outputsCollection.ts +++ b/packages/core/src/models/collections/outputsCollection.ts @@ -18,10 +18,7 @@ function setSum(map: Map, key: K, value: bigint) { export class OutputsCollection extends Collection { constructor(outputs?: OneOrMore) { super(); - - if (isDefined(outputs)) { - this.add(outputs); - } + if (isDefined(outputs)) this.add(outputs); } protected _map(output: OutputBuilder) { diff --git a/packages/core/src/models/collections/tokensCollection.ts b/packages/core/src/models/collections/tokensCollection.ts index aed9f1a..f729a99 100644 --- a/packages/core/src/models/collections/tokensCollection.ts +++ b/packages/core/src/models/collections/tokensCollection.ts @@ -34,10 +34,7 @@ export class TokensCollection extends Collection, OutputToke constructor(tokens: TokenAmount[], options: TokenAddOptions); constructor(tokens?: OneOrMore>, options?: TokenAddOptions) { super(); - - if (isDefined(tokens)) { - this.add(tokens, options); - } + if (isDefined(tokens)) this.add(tokens, options); } public get minting(): NewToken | undefined { diff --git a/plugins/babel-fees/src/plugins.ts b/plugins/babel-fees/src/plugins.ts index 2f97b8e..765c4b4 100644 --- a/plugins/babel-fees/src/plugins.ts +++ b/plugins/babel-fees/src/plugins.ts @@ -15,9 +15,7 @@ export function BabelSwapPlugin( babelBox: Box, token: TokenAmount ): FleetPlugin { - if (!isValidBabelBox(babelBox)) { - throw new Error("Invalid Babel Box."); - } + if (!isValidBabelBox(babelBox)) throw new Error("Invalid Babel Box."); if (!isBabelContractForTokenId(babelBox.ergoTree, token.tokenId)) { throw new Error( @@ -42,14 +40,10 @@ export function BabelSwapPlugin( .setAdditionalRegisters({ R4: input.additionalRegisters.R4, R5: input.additionalRegisters.R5, - R6: SColl(SByte, input.boxId).toHex() + R6: SColl(SByte, input.boxId) }) ); - addInputs( - input.setContextVars({ - 0: SInt(outputsLength - 1).toHex() - }) - ); + addInputs(input.setContextExtension({ 0: SInt(outputsLength - 1) })); }; } From 85f049d0247b98401abd20d741b8abf37c46e62f Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Sat, 14 Sep 2024 21:21:13 -0300 Subject: [PATCH 2/8] add `ensureInclusion` param --- packages/core/src/builder/pluginContext.ts | 8 ++----- .../core/src/builder/transactionBuilder.ts | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/core/src/builder/pluginContext.ts b/packages/core/src/builder/pluginContext.ts index 698cec0..78bca2c 100644 --- a/packages/core/src/builder/pluginContext.ts +++ b/packages/core/src/builder/pluginContext.ts @@ -58,12 +58,8 @@ export function createPluginContext( return { addInputs: (inputs) => transactionBuilder - .from(inputs) - .configureSelector((selector) => - selector.ensureInclusion( - Array.isArray(inputs) ? inputs.map((input) => input.boxId) : inputs.boxId - ) - ).inputs.length, + .from(inputs, { ensureInclusion: true }) + .inputs.length, addOutputs: (outputs, options) => transactionBuilder.to(outputs, options).outputs.length, addDataInputs: (dataInputs, options) => diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index 646fe24..a7e2b78 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -72,12 +72,12 @@ type InputsInclusionOptions = { export class TransactionBuilder { private readonly _inputs!: InputsCollection; - private readonly _preservedInputs!: InputsCollection; private readonly _dataInputs!: InputsCollection; private readonly _outputs!: OutputsCollection; private readonly _settings!: TransactionBuilderSettings; private readonly _creationHeight!: number; + private _ensureInclusion?: Set; private _selectorCallbacks?: SelectorCallback[]; private _changeAddress?: ErgoAddress; private _feeAmount?: bigint; @@ -143,15 +143,22 @@ export class TransactionBuilder { options: InputsInclusionOptions = { ensureInclusion: false } ): TransactionBuilder { const items = isCollectionLike(inputs) ? inputs.toArray() : inputs; - if (options.ensureInclusion) { - this._preservedInputs.add(items); - } else { - this._inputs.add(items); - } + if (options.ensureInclusion) this.#ensureInclusionOf(items); + this._inputs.add(items); return this; } + #ensureInclusionOf(inputs: OneOrMore>): void { + if (!this._ensureInclusion) this._ensureInclusion = new Set(); + + if (Array.isArray(inputs)) { + for (const input of inputs) this._ensureInclusion.add(input.boxId); + } else { + this._ensureInclusion.add(inputs.boxId); + } + } + public to( outputs: OneOrMore, options?: CollectionAddOptions @@ -274,6 +281,10 @@ export class TransactionBuilder { } const selector = new BoxSelector(this.inputs.toArray()); + if (this._ensureInclusion?.size) { + selector.ensureInclusion(Array.from(this._ensureInclusion)); + } + if (some(this._selectorCallbacks)) { for (const selectorCallBack of this._selectorCallbacks) { selectorCallBack(selector); From 029faa8e475b2d967bc07e1549b99a540eb3755c Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Sat, 14 Sep 2024 21:24:31 -0300 Subject: [PATCH 3/8] clean up --- packages/core/src/builder/pluginContext.ts | 22 ++++++------------- .../core/src/builder/transactionBuilder.ts | 4 ++-- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/core/src/builder/pluginContext.ts b/packages/core/src/builder/pluginContext.ts index 78bca2c..2be5012 100644 --- a/packages/core/src/builder/pluginContext.ts +++ b/packages/core/src/builder/pluginContext.ts @@ -52,24 +52,16 @@ export type FleetPluginContext = { setFee: (amount: Amount) => void; }; -export function createPluginContext( - transactionBuilder: TransactionBuilder -): FleetPluginContext { +export function createPluginContext(builder: TransactionBuilder): FleetPluginContext { return { - addInputs: (inputs) => - transactionBuilder - .from(inputs, { ensureInclusion: true }) - .inputs.length, - addOutputs: (outputs, options) => - transactionBuilder.to(outputs, options).outputs.length, + addInputs: (inputs) => builder.from(inputs, { ensureInclusion: true }).inputs.length, + addOutputs: (outputs, options) => builder.to(outputs, options).outputs.length, addDataInputs: (dataInputs, options) => - transactionBuilder.withDataFrom(dataInputs, options).dataInputs.length, + builder.withDataFrom(dataInputs, options).dataInputs.length, burnTokens: (tokens) => { - if (!transactionBuilder.settings.canBurnTokensFromPlugins) { - throw new NotAllowedTokenBurning(); - } - transactionBuilder.burnTokens(tokens); + if (!builder.settings.canBurnTokensFromPlugins) throw new NotAllowedTokenBurning(); + builder.burnTokens(tokens); }, - setFee: (amount) => transactionBuilder.payFee(amount) + setFee: (amount) => builder.payFee(amount) }; } diff --git a/packages/core/src/builder/transactionBuilder.ts b/packages/core/src/builder/transactionBuilder.ts index a7e2b78..bbc141d 100644 --- a/packages/core/src/builder/transactionBuilder.ts +++ b/packages/core/src/builder/transactionBuilder.ts @@ -281,10 +281,10 @@ export class TransactionBuilder { } const selector = new BoxSelector(this.inputs.toArray()); - if (this._ensureInclusion?.size) { + if (this._ensureInclusion?.size) { selector.ensureInclusion(Array.from(this._ensureInclusion)); } - + if (some(this._selectorCallbacks)) { for (const selectorCallBack of this._selectorCallbacks) { selectorCallBack(selector); From b119f4057649ee47858c17d6ead8af771ccc4c75 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:24:25 -0300 Subject: [PATCH 4/8] tweak plugin context to use `ensureInclusion` flag --- packages/core/src/builder/pluginContext.spec.ts | 15 ++++++--------- packages/core/src/builder/pluginContext.ts | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/core/src/builder/pluginContext.spec.ts b/packages/core/src/builder/pluginContext.spec.ts index 18ed862..c298a03 100644 --- a/packages/core/src/builder/pluginContext.spec.ts +++ b/packages/core/src/builder/pluginContext.spec.ts @@ -17,29 +17,26 @@ describe("Plugin context", () => { it("Should add inputs", () => { const fromSpy = vi.spyOn(builder, "from"); - const configureSelectorMethod = vi.spyOn(builder, "configureSelector"); - const newLen = context.addInputs(regularBoxes); expect(fromSpy).toBeCalledTimes(1); - expect(configureSelectorMethod).toBeCalledTimes(1); + expect(fromSpy).toBeCalledWith(regularBoxes, { ensureInclusion: true }); expect(builder.inputs).toHaveLength(newLen); expect(builder.inputs).toHaveLength(regularBoxes.length); }); it("Should add a single input", () => { - const fromMethod = vi.spyOn(builder, "from"); - const configureSelectorMethod = vi.spyOn(builder, "configureSelector"); + const fromSpy = vi.spyOn(builder, "from"); + const inputs = regularBoxes[0]; let newLen = context.addInputs(regularBoxes[0]); - expect(fromMethod).toBeCalledTimes(1); - expect(configureSelectorMethod).toBeCalledTimes(1); + expect(fromSpy).toBeCalledTimes(1); + expect(fromSpy).toBeCalledWith(inputs, { ensureInclusion: true }); expect(builder.inputs).toHaveLength(newLen); expect(builder.inputs).toHaveLength(1); newLen = context.addInputs(regularBoxes[1]); - expect(fromMethod).toBeCalledTimes(2); - expect(configureSelectorMethod).toBeCalledTimes(2); + expect(fromSpy).toBeCalledTimes(2); expect(builder.inputs).toHaveLength(newLen); expect(builder.inputs).toHaveLength(2); }); diff --git a/packages/core/src/builder/pluginContext.ts b/packages/core/src/builder/pluginContext.ts index 2be5012..994242e 100644 --- a/packages/core/src/builder/pluginContext.ts +++ b/packages/core/src/builder/pluginContext.ts @@ -5,7 +5,7 @@ import type { OneOrMore, TokenAmount } from "@fleet-sdk/common"; -import { NotAllowedTokenBurning, type OutputBuilder, type TransactionBuilder } from ".."; +import { type OutputBuilder, type TransactionBuilder, NotAllowedTokenBurning } from ".."; export type FleetPluginContext = { /** From a7f43724f7bd12272cf41369ae072ec5ddf1fb04 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:07:40 -0300 Subject: [PATCH 5/8] add inclusion and order test --- .../core/src/builder/transactionBuilder.spec.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/builder/transactionBuilder.spec.ts b/packages/core/src/builder/transactionBuilder.spec.ts index a3766bc..f3b6401 100644 --- a/packages/core/src/builder/transactionBuilder.spec.ts +++ b/packages/core/src/builder/transactionBuilder.spec.ts @@ -818,6 +818,19 @@ describe("Building", () => { } }); + it("Should ensure inclusion and order or inputs", () => { + const transaction = new TransactionBuilder(height) + .from(regularBoxes) + .and.from(manyTokensBoxes, { ensureInclusion: true }) + .sendChangeTo(a1.address) + .build(); + + expect(transaction.inputs).to.have.length(manyTokensBoxes.length); + for (let i = 0; i < transaction.inputs.length; i++) { + expect(transaction.inputs[i].boxId).to.be.equal(manyTokensBoxes[i].boxId); + } + }); + it("Should produce multiple change boxes based on maxTokensPerChangeBox and isolate erg in a exclusive box ", () => { const tokensPerBox = 3; @@ -908,7 +921,7 @@ describe("Building", () => { it("Should preserve inputs extension", () => { const input = new ErgoUnsignedInput(regularBoxes[0]); - input.setContextVars({ 0: "0580c0fc82aa02" }); + input.setContextExtension({ 0: "0580c0fc82aa02" }); const unsignedTransaction = new TransactionBuilder(height) .from(regularBoxes[1]) From a08a573b1c8357de33419e0f73e65c0203cd03a9 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:10:03 -0300 Subject: [PATCH 6/8] add changeset --- .changeset/hip-maps-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-maps-sin.md diff --git a/.changeset/hip-maps-sin.md b/.changeset/hip-maps-sin.md new file mode 100644 index 0000000..94b5c11 --- /dev/null +++ b/.changeset/hip-maps-sin.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/core": patch +--- + +Add `ensureInclusion` flag to the `TransactionBuilder#from` method to ensure both inclusion and order of inputs From da29dd79848e5446315a9b8ee77efb2e912c0f9c Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:25:21 -0300 Subject: [PATCH 7/8] double check inputs order preserving --- .../core/src/builder/transactionBuilder.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/core/src/builder/transactionBuilder.spec.ts b/packages/core/src/builder/transactionBuilder.spec.ts index f3b6401..c439fa5 100644 --- a/packages/core/src/builder/transactionBuilder.spec.ts +++ b/packages/core/src/builder/transactionBuilder.spec.ts @@ -25,6 +25,7 @@ import { RECOMMENDED_MIN_FEE_VALUE, TransactionBuilder } from "./transactionBuilder"; +import { mockUTxO } from "packages/mock-chain/src"; const height = 844540; const a1 = { @@ -818,16 +819,19 @@ describe("Building", () => { } }); - it("Should ensure inclusion and order or inputs", () => { + it("Should ensure inclusion and preserver the order or inputs", () => { + const anotherInput = mockUTxO({ ergoTree: a1.ergoTree }); const transaction = new TransactionBuilder(height) - .from(regularBoxes) - .and.from(manyTokensBoxes, { ensureInclusion: true }) + .from(manyTokensBoxes, { ensureInclusion: true }) + .and.from(regularBoxes) + .and.from(anotherInput, { ensureInclusion: true }) .sendChangeTo(a1.address) .build(); - expect(transaction.inputs).to.have.length(manyTokensBoxes.length); + const expectedInputs = [...manyTokensBoxes, anotherInput]; + expect(transaction.inputs).to.have.length(expectedInputs.length); for (let i = 0; i < transaction.inputs.length; i++) { - expect(transaction.inputs[i].boxId).to.be.equal(manyTokensBoxes[i].boxId); + expect(transaction.inputs[i].boxId).to.be.equal(expectedInputs[i].boxId); } }); From 10170e6d6727e184b02e31b25a3bc256043672f6 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:04:43 -0300 Subject: [PATCH 8/8] adjust changeset --- .changeset/hip-maps-sin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/hip-maps-sin.md b/.changeset/hip-maps-sin.md index 94b5c11..27779fc 100644 --- a/.changeset/hip-maps-sin.md +++ b/.changeset/hip-maps-sin.md @@ -1,5 +1,5 @@ --- -"@fleet-sdk/core": patch +"@fleet-sdk/core": minor --- Add `ensureInclusion` flag to the `TransactionBuilder#from` method to ensure both inclusion and order of inputs