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

Add chained transaction building #148

Merged
merged 17 commits into from
Oct 24, 2024
5 changes: 5 additions & 0 deletions .changeset/forty-trains-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/core": minor
---

Add transaction chain building
2 changes: 1 addition & 1 deletion packages/common/src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export type SortingDirection = "asc" | "desc";

export type FilterPredicate<T> = (item: T) => boolean;

export type BuildOutputType = "default" | "EIP-12";
export type PlainObjectType = "minimal" | "EIP-12";
23 changes: 17 additions & 6 deletions packages/core/src/builder/outputBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ describe("Constructor", () => {
});
});

describe("flags", () => {
it("Should set default flags", () => {
const builder = new OutputBuilder(SAFE_MIN_BOX_VALUE, address);
expect(builder.flags).toEqual({ change: false });
});

it("Should set flags", () => {
const builder = new OutputBuilder(SAFE_MIN_BOX_VALUE, address);

builder.setFlags({ change: true });
expect(builder.flags).toEqual({ change: true });

builder.setFlags({ change: false });
expect(builder.flags).toEqual({ change: false });
});
});

describe("Creation height", () => {
it("Should construct with no creation height and set it using setCreationHeight()", () => {
const builder = new OutputBuilder(SAFE_MIN_BOX_VALUE, address);
Expand Down Expand Up @@ -347,7 +364,6 @@ describe("Building", () => {
it("Should build box without tokens", () => {
const boxCandidate = new OutputBuilder(SAFE_MIN_BOX_VALUE, address, height).build();

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand All @@ -361,7 +377,6 @@ describe("Building", () => {
.addTokens({ tokenId: tokenB, amount: 1n })
.build();

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand All @@ -382,7 +397,6 @@ describe("Building", () => {
})
.build(regularBoxes);

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand Down Expand Up @@ -411,7 +425,6 @@ describe("Building", () => {
})
.build(regularBoxes);

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand Down Expand Up @@ -452,7 +465,6 @@ describe("Building", () => {
})
.build(regularBoxes);

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand All @@ -478,7 +490,6 @@ describe("Building", () => {
.setAdditionalRegisters({ R4: "0e00" })
.build(regularBoxes);

expect(boxCandidate.boxId).toBeUndefined();
expect(boxCandidate.value).toEqual(SAFE_MIN_BOX_VALUE);
expect(boxCandidate.ergoTree).toEqual(ErgoAddress.fromBase58(address).ergoTree);
expect(boxCandidate.creationHeight).toEqual(height);
Expand Down
124 changes: 67 additions & 57 deletions packages/core/src/builder/outputBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import {
type TokenAddOptions,
TokensCollection
} from "../models/collections/tokensCollection";
import { ErgoBoxCandidate } from "../models/ergoBoxCandidate";

export const BOX_VALUE_PER_BYTE = BigInt(360);
export const SAFE_MIN_BOX_VALUE = BigInt(1000000);

export type BoxValueEstimationCallback = (outputBuilder: OutputBuilder) => bigint;
export type TransactionOutputFlags = { change: boolean };

export function estimateMinBoxValue(
valuePerByte = BOX_VALUE_PER_BYTE
Expand All @@ -45,12 +47,13 @@ export function estimateMinBoxValue(
const DUMB_TOKEN_ID = "0000000000000000000000000000000000000000000000000000000000000000";

export class OutputBuilder {
private readonly _address: ErgoAddress;
private readonly _tokens: TokensCollection;
private _value!: bigint;
private _valueEstimator?: BoxValueEstimationCallback;
private _creationHeight?: number;
private _registers: NonMandatoryRegisters;
readonly #address: ErgoAddress;
readonly #tokens: TokensCollection;
#value!: bigint;
#valueEstimator?: BoxValueEstimationCallback;
#creationHeight?: number;
#registers: NonMandatoryRegisters;
#flags: TransactionOutputFlags = { change: false };

constructor(
value: Amount | BoxValueEstimationCallback,
Expand All @@ -59,104 +62,110 @@ export class OutputBuilder {
) {
this.setValue(value);

this._creationHeight = creationHeight;
this._tokens = new TokensCollection();
this._registers = {};
this.#creationHeight = creationHeight;
this.#tokens = new TokensCollection();
this.#registers = {};
this.#flags = { change: false };

if (typeof recipient === "string") {
this._address = isHex(recipient)
this.#address = isHex(recipient)
? ErgoAddress.fromErgoTree(recipient)
: ErgoAddress.fromBase58(recipient);
} else if (recipient instanceof ErgoTree) {
this._address = recipient.toAddress();
this.#address = recipient.toAddress();
} else {
this._address = recipient;
this.#address = recipient;
}
}

public get value(): bigint {
return isDefined(this._valueEstimator) ? this._valueEstimator(this) : this._value;
get value(): bigint {
return isDefined(this.#valueEstimator) ? this.#valueEstimator(this) : this.#value;
}

public get address(): ErgoAddress {
return this._address;
get address(): ErgoAddress {
return this.#address;
}

public get ergoTree(): ErgoTreeHex {
return this._address.ergoTree;
get ergoTree(): ErgoTreeHex {
return this.#address.ergoTree;
}

public get creationHeight(): number | undefined {
return this._creationHeight;
get creationHeight(): number | undefined {
return this.#creationHeight;
}

public get assets(): TokensCollection {
return this._tokens;
get assets(): TokensCollection {
return this.#tokens;
}

public get additionalRegisters(): NonMandatoryRegisters {
return this._registers;
get additionalRegisters(): NonMandatoryRegisters {
return this.#registers;
}

public get minting(): NewToken<bigint> | undefined {
get minting(): NewToken<bigint> | undefined {
return this.assets.minting;
}

public setValue(value: Amount | BoxValueEstimationCallback): OutputBuilder {
get flags(): TransactionOutputFlags {
return this.#flags;
}

setValue(value: Amount | BoxValueEstimationCallback): OutputBuilder {
if (typeof value === "function") {
this._valueEstimator = value;
this.#valueEstimator = value;
} else {
this._value = ensureBigInt(value);
this._valueEstimator = undefined;
this.#value = ensureBigInt(value);
this.#valueEstimator = undefined;

if (this._value <= _0n) {
if (this.#value <= _0n) {
throw new Error("An UTxO cannot be created without a minimum required amount.");
}
}

return this;
}

public addTokens(
setFlags(flags: Partial<TransactionOutputFlags>): OutputBuilder {
this.#flags = { ...this.#flags, ...flags };
return this;
}

addTokens(
tokens: OneOrMore<TokenAmount<Amount>> | TokensCollection,
options?: TokenAddOptions
): OutputBuilder {
if (tokens instanceof TokensCollection) {
this._tokens.add(tokens.toArray(), options);
this.#tokens.add(tokens.toArray(), options);
} else {
this._tokens.add(tokens, options);
this.#tokens.add(tokens, options);
}

return this;
}

public addNfts(...tokenIds: TokenId[]): OutputBuilder {
addNfts(...tokenIds: TokenId[]): OutputBuilder {
const tokens = tokenIds.map((tokenId) => ({ tokenId, amount: _1n }));

return this.addTokens(tokens);
}

public mintToken(token: NewToken<Amount>): OutputBuilder {
mintToken(token: NewToken<Amount>): OutputBuilder {
this.assets.mint(token);
return this;
}

public setCreationHeight(
height: number,
options?: { replace: boolean }
): OutputBuilder {
setCreationHeight(height: number, options?: { replace: boolean }): OutputBuilder {
if (
isUndefined(options) ||
options.replace === true ||
(options.replace === false && isUndefined(this._creationHeight))
(options.replace === false && isUndefined(this.#creationHeight))
) {
this._creationHeight = height;
this.#creationHeight = height;
}

return this;
}

public setAdditionalRegisters<T extends AdditionalRegistersInput>(
setAdditionalRegisters<T extends AdditionalRegistersInput>(
registers: SequentialNonMandatoryRegisters<T>
): OutputBuilder {
const hexRegisters: NonMandatoryRegisters = {};
Expand All @@ -169,19 +178,17 @@ export class OutputBuilder {
}

if (!areRegistersDenselyPacked(hexRegisters)) throw new InvalidRegistersPacking();
this._registers = hexRegisters;
this.#registers = hexRegisters;

return this;
}

public eject(ejector: (context: { tokens: TokensCollection }) => void): OutputBuilder {
ejector({ tokens: this._tokens });
eject(ejector: (context: { tokens: TokensCollection }) => void): OutputBuilder {
ejector({ tokens: this.#tokens });
return this;
}

public build(
transactionInputs?: UnsignedInput[] | Box<Amount>[]
): BoxCandidate<bigint> {
build(transactionInputs?: UnsignedInput[] | Box<Amount>[]): ErgoBoxCandidate {
let tokens: TokenAmount<bigint>[];

if (this.minting) {
Expand All @@ -201,13 +208,16 @@ export class OutputBuilder {

if (isUndefined(this.creationHeight)) throw new UndefinedCreationHeight();

return {
value: this.value,
ergoTree: this.ergoTree,
creationHeight: this.creationHeight,
assets: tokens,
additionalRegisters: this.additionalRegisters
};
return new ErgoBoxCandidate(
{
value: this.value,
ergoTree: this.ergoTree,
creationHeight: this.creationHeight,
assets: tokens,
additionalRegisters: this.additionalRegisters
},
this.#flags
);
}

estimateSize(value = SAFE_MIN_BOX_VALUE): number {
Expand All @@ -217,7 +227,7 @@ export class OutputBuilder {
value,
ergoTree: this.ergoTree,
creationHeight: this.creationHeight,
assets: this._tokens.toArray(DUMB_TOKEN_ID),
assets: this.#tokens.toArray(DUMB_TOKEN_ID),
additionalRegisters: this.additionalRegisters
};

Expand Down
20 changes: 14 additions & 6 deletions packages/core/src/builder/transactionBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ describe("basic construction", () => {
expect(builder.fee).toBe(fee);
});

it("Should update creation height", () => {
const builder = new TransactionBuilder(1);
expect(builder.creationHeight).to.be.equal(1);

builder.atHeight(2);
expect(builder.creationHeight).to.be.equal(2);
});

it("Should set min recommended fee amount", () => {
const builder = new TransactionBuilder(height).from(regularBoxes).payMinFee();

Expand Down Expand Up @@ -431,7 +439,7 @@ describe("Building", () => {
.build()
.toEIP12Object();

expect(tx.outputs).toEqual([
expect(tx.outputs).to.deep.equal([
expectedSendingBox,
expectedBabelRecreatedBox,
expectedFeeBox,
Expand Down Expand Up @@ -750,13 +758,13 @@ describe("Building", () => {

expect(change2.ergoTree).toBe(a1.ergoTree);
expect(change2.creationHeight).toBe(height);
expect(change2.value).toBe(_estimateBoxValue(change2));
expect(change2.value).toBe(_estimateBoxValue(change2.toCandidate()));
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(_estimateBoxValue(change3));
expect(change3.value).toBe(_estimateBoxValue(change3.toCandidate()));
expect(change3.assets).toHaveLength(72);
expect(change3.additionalRegisters).toEqual({});
});
Expand Down Expand Up @@ -788,7 +796,7 @@ describe("Building", () => {

if (i > 0) {
expect(transaction.outputs[i].value).toBe(
_estimateBoxValue(transaction.outputs[i])
_estimateBoxValue(transaction.outputs[i].toCandidate())
);
}
}
Expand All @@ -813,7 +821,7 @@ describe("Building", () => {

if (i > 0) {
expect(transaction.outputs[i].value).toBe(
_estimateBoxValue(transaction.outputs[i])
_estimateBoxValue(transaction.outputs[i].toCandidate())
);
}
}
Expand Down Expand Up @@ -858,7 +866,7 @@ describe("Building", () => {
for (let i = 1; i < transaction.outputs.length; i++) {
expect(transaction.outputs[i].assets).toHaveLength(tokensPerBox);
expect(transaction.outputs[i].value).toBe(
_estimateBoxValue(transaction.outputs[i])
_estimateBoxValue(transaction.outputs[i].toCandidate())
);
}
});
Expand Down
Loading
Loading