Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure inclusion and order of inputs #134

Merged
merged 9 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-maps-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/core": minor
---

Add `ensureInclusion` flag to the `TransactionBuilder#from` method to ensure both inclusion and order of inputs
15 changes: 6 additions & 9 deletions packages/core/src/builder/pluginContext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
28 changes: 8 additions & 20 deletions packages/core/src/builder/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -52,28 +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)
.configureSelector((selector) =>
selector.ensureInclusion(
Array.isArray(inputs) ? inputs.map((input) => input.boxId) : inputs.boxId
)
).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)
};
}
16 changes: 5 additions & 11 deletions packages/core/src/builder/selector/boxSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ export class BoxSelector<T extends Box<bigint>> {
}

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));
}
Expand All @@ -93,9 +91,7 @@ export class BoxSelector<T extends Box<bigint>> {

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);
}
}
}
Expand Down Expand Up @@ -183,9 +179,7 @@ export class BoxSelector<T extends Box<bigint>> {
}

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);
}
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/builder/transactionBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
RECOMMENDED_MIN_FEE_VALUE,
TransactionBuilder
} from "./transactionBuilder";
import { mockUTxO } from "packages/mock-chain/src";

const height = 844540;
const a1 = {
Expand Down Expand Up @@ -818,6 +819,22 @@ describe("Building", () => {
}
});

it("Should ensure inclusion and preserver the order or inputs", () => {
const anotherInput = mockUTxO({ ergoTree: a1.ergoTree });
const transaction = new TransactionBuilder(height)
.from(manyTokensBoxes, { ensureInclusion: true })
.and.from(regularBoxes)
.and.from(anotherInput, { ensureInclusion: true })
.sendChangeTo(a1.address)
.build();

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(expectedInputs[i].boxId);
}
});

it("Should produce multiple change boxes based on maxTokensPerChangeBox and isolate erg in a exclusive box ", () => {
const tokensPerBox = 3;

Expand Down Expand Up @@ -908,7 +925,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])
Expand Down
59 changes: 40 additions & 19 deletions packages/core/src/builder/transactionBuilder.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -59,13 +59,25 @@ 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 _dataInputs!: InputsCollection;
private readonly _outputs!: OutputsCollection;
private readonly _settings!: TransactionBuilderSettings;
private readonly _creationHeight!: number;

private _ensureInclusion?: Set<string>;
private _selectorCallbacks?: SelectorCallback[];
private _changeAddress?: ErgoAddress;
private _feeAmount?: bigint;
Expand Down Expand Up @@ -127,12 +139,26 @@ export class TransactionBuilder {
}

public from(
inputs: OneOrMore<Box<Amount>> | CollectionLike<Box<Amount>>
inputs: OneOrMore<Box<Amount>> | CollectionLike<Box<Amount>>,
options: InputsInclusionOptions = { ensureInclusion: false }
): TransactionBuilder {
this._inputs.add(isCollectionLike(inputs) ? inputs.toArray() : inputs);
const items = isCollectionLike(inputs) ? inputs.toArray() : inputs;
if (options.ensureInclusion) this.#ensureInclusionOf(items);

this._inputs.add(items);
return this;
}

#ensureInclusionOf(inputs: OneOrMore<Box<Amount>>): 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<OutputBuilder>,
options?: CollectionAddOptions
Expand Down Expand Up @@ -193,19 +219,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;
Expand Down Expand Up @@ -261,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);
Expand Down Expand Up @@ -428,7 +452,6 @@ type ChangeEstimationParams = {

function estimateMinChangeValue(params: ChangeEstimationParams): bigint {
const size = BigInt(estimateChangeSize(params));

return size * BOX_VALUE_PER_BYTE;
}

Expand All @@ -448,9 +471,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);
Expand Down
10 changes: 3 additions & 7 deletions packages/core/src/models/collections/inputsCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -15,10 +15,7 @@ export class InputsCollection extends Collection<ErgoUnsignedInput, Box<Amount>>
constructor(boxes: Box<Amount>[]);
constructor(boxes?: OneOrMore<Box<Amount>>) {
super();

if (isDefined(boxes)) {
this.add(boxes);
}
if (isDefined(boxes)) this.add(boxes);
}

protected override _map(input: Box<Amount> | ErgoUnsignedInput): ErgoUnsignedInput {
Expand Down Expand Up @@ -54,7 +51,6 @@ export class InputsCollection extends Collection<ErgoUnsignedInput, Box<Amount>>
}

this._items.splice(index, 1);

return this.length;
}
}
5 changes: 1 addition & 4 deletions packages/core/src/models/collections/outputsCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ function setSum<K>(map: Map<K, bigint>, key: K, value: bigint) {
export class OutputsCollection extends Collection<OutputBuilder, OutputBuilder> {
constructor(outputs?: OneOrMore<OutputBuilder>) {
super();

if (isDefined(outputs)) {
this.add(outputs);
}
if (isDefined(outputs)) this.add(outputs);
}

protected _map(output: OutputBuilder) {
Expand Down
5 changes: 1 addition & 4 deletions packages/core/src/models/collections/tokensCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ export class TokensCollection extends Collection<OutputToken<bigint>, OutputToke
constructor(tokens: TokenAmount<Amount>[], options: TokenAddOptions);
constructor(tokens?: OneOrMore<TokenAmount<Amount>>, options?: TokenAddOptions) {
super();

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

public get minting(): NewToken<bigint> | undefined {
Expand Down
12 changes: 3 additions & 9 deletions plugins/babel-fees/src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ export function BabelSwapPlugin(
babelBox: Box<Amount>,
token: TokenAmount<Amount>
): 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(
Expand All @@ -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) }));
};
}
Loading