Skip to content

Commit

Permalink
Merge pull request #148 from fleet-sdk/add-tx-chaining
Browse files Browse the repository at this point in the history
Add chained transaction building
  • Loading branch information
arobsn authored Oct 24, 2024
2 parents 67eff19 + 9f8b5ee commit b0f25d5
Show file tree
Hide file tree
Showing 17 changed files with 799 additions and 352 deletions.
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

0 comments on commit b0f25d5

Please sign in to comment.