From ddd41b29fbde02e0ce7a675d402668383636ae1d Mon Sep 17 00:00:00 2001 From: lisicky Date: Mon, 14 Sep 2026 14:35:55 +0200 Subject: [PATCH] add donation field --- .../src/types/transaction-builder/index.ts | 1 + .../src/core/adaptor/fromObj/index.ts | 4 + .../src/core/adaptor/toObj/index.ts | 2 + packages/mesh-core-csl/src/core/serializer.ts | 5 ++ .../mesh-core-cst/src/serializer/index.ts | 8 ++ .../src/mesh-tx-builder/index.ts | 6 +- .../src/mesh-tx-builder/tx-builder-core.ts | 11 +++ .../test/mesh-tx-builder/donation.test.ts | 79 +++++++++++++++++++ 8 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/mesh-transaction/test/mesh-tx-builder/donation.test.ts diff --git a/packages/mesh-common/src/types/transaction-builder/index.ts b/packages/mesh-common/src/types/transaction-builder/index.ts index 7735ede55..6175683da 100644 --- a/packages/mesh-common/src/types/transaction-builder/index.ts +++ b/packages/mesh-common/src/types/transaction-builder/index.ts @@ -45,6 +45,7 @@ export type MeshTxBuilderBody = { expectedByronAddressWitnesses: string[]; totalCollateral?: Quantity; collateralReturnAddress?: string; + donation?: Quantity; }; export const emptyTxBuilderBody = (): MeshTxBuilderBody => ({ diff --git a/packages/mesh-core-csl/src/core/adaptor/fromObj/index.ts b/packages/mesh-core-csl/src/core/adaptor/fromObj/index.ts index b200ed108..e01b86c93 100644 --- a/packages/mesh-core-csl/src/core/adaptor/fromObj/index.ts +++ b/packages/mesh-core-csl/src/core/adaptor/fromObj/index.ts @@ -125,6 +125,10 @@ export const txBuilderBodyFromObj = (objJson: any): MeshTxBuilderBody => { txBuilderBody.network = networkFromObj(obj.network); } + if (obj.donation) { + txBuilderBody.donation = obj.donation.toString(); + } + return txBuilderBody; }; diff --git a/packages/mesh-core-csl/src/core/adaptor/toObj/index.ts b/packages/mesh-core-csl/src/core/adaptor/toObj/index.ts index 24e7854e4..c6f85d71a 100644 --- a/packages/mesh-core-csl/src/core/adaptor/toObj/index.ts +++ b/packages/mesh-core-csl/src/core/adaptor/toObj/index.ts @@ -29,6 +29,7 @@ export const meshTxBuilderBodyToObj = ({ votes, fee, network, + donation, }: MeshTxBuilderBody) => { let mintsObj: object[] = []; mints.forEach((mint: MintParam) => { @@ -61,6 +62,7 @@ export const meshTxBuilderBodyToObj = ({ votes: votes.map(voteToObj), fee, network: networkToObj(network), + ...(donation !== undefined ? { donation } : {}), }; }; diff --git a/packages/mesh-core-csl/src/core/serializer.ts b/packages/mesh-core-csl/src/core/serializer.ts index 0c92d2c45..b635c73a2 100644 --- a/packages/mesh-core-csl/src/core/serializer.ts +++ b/packages/mesh-core-csl/src/core/serializer.ts @@ -75,6 +75,11 @@ export class CSLSerializer implements IMeshTxSerializer { txBody: MeshTxBuilderBody, protocolParams?: Protocol, ): string { + if (txBody.donation) { + throw new Error( + "Treasury donation is not supported by the CSL serializer, use CSTSerializer instead", + ); + } const txBodyJson = JSONbig.stringify(meshTxBuilderBodyToObj(txBody)); const params = JSONbig.stringify(protocolParams || this.protocolParams); diff --git a/packages/mesh-core-cst/src/serializer/index.ts b/packages/mesh-core-cst/src/serializer/index.ts index 4e7f13808..d983069d6 100644 --- a/packages/mesh-core-cst/src/serializer/index.ts +++ b/packages/mesh-core-cst/src/serializer/index.ts @@ -619,6 +619,7 @@ class CardanoSDKSerializerCore { totalCollateral, collateralReturnAddress, changeAddress, + donation, } = txBuilderBody; const uniqueRefInputs = this.removeBodyInputRefInputOverlap( @@ -690,6 +691,13 @@ class CardanoSDKSerializerCore { ); } } + if (donation) { + try { + this.txBody.setDonation(BigInt(donation)); + } catch (e) { + throwErrorWithOrigin("Error serializing donation", e); + } + } try { this.addAllReferenceInputs(uniqueRefInputs); } catch (e) { diff --git a/packages/mesh-transaction/src/mesh-tx-builder/index.ts b/packages/mesh-transaction/src/mesh-tx-builder/index.ts index b8c1df9b1..77db15266 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/index.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/index.ts @@ -256,7 +256,7 @@ export class MeshTxBuilder extends MeshTxBuilderCore { const utxosForSelection = await this.getUtxosForSelection(); const implicitValue = { withdrawals: this.getTotalWithdrawal(), - deposit: this.getTotalDeposit(), + deposit: this.getTotalDeposit() + this.getDonation(), reclaimDeposit: this.getTotalRefund(), mint: this.getTotalMint(), }; @@ -1311,6 +1311,10 @@ export class MeshTxBuilder extends MeshTxBuilderCore { return accum; }; + protected getDonation = (): bigint => { + return BigInt(this.meshTxBuilderBody.donation ?? 0); + }; + protected getTotalRefund = (): bigint => { let accum = 0n; for (let cert of this.meshTxBuilderBody.certificates) { diff --git a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts index efbdcefeb..da2935063 100644 --- a/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts +++ b/packages/mesh-transaction/src/mesh-tx-builder/tx-builder-core.ts @@ -1659,6 +1659,17 @@ export class MeshTxBuilderCore { return this; }; + /** + * Sets the treasury donation in lovelace. + */ + setDonation = (donation: string) => { + if (BigInt(donation) <= 0n) { + throw new Error("Donation must be a positive amount of lovelace"); + } + this.meshTxBuilderBody.donation = donation; + return this; + }; + /** * Sets the network to use, this is mainly to know the cost models to be used to calculate script integrity hash * @param network The specific network this transaction is being built for ("testnet" | "preview" | "preprod" | "mainnet") diff --git a/packages/mesh-transaction/test/mesh-tx-builder/donation.test.ts b/packages/mesh-transaction/test/mesh-tx-builder/donation.test.ts new file mode 100644 index 000000000..e0ecac013 --- /dev/null +++ b/packages/mesh-transaction/test/mesh-tx-builder/donation.test.ts @@ -0,0 +1,79 @@ +import { CSLSerializer } from "@meshsdk/core-csl"; +import { Transaction, TxCBOR } from "@meshsdk/core-cst"; +import { MeshTxBuilder } from "@meshsdk/transaction"; + +import { calculateOutputLovelaces, txHash } from "../test-util"; + +const address = + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9"; + +describe("MeshTxBuilder donation", () => { + let txBuilder: MeshTxBuilder; + + beforeEach(() => { + txBuilder = new MeshTxBuilder(); + }); + + it("Should set donation in the tx body and account for it when balancing", async () => { + const tx = await txBuilder + .txIn( + txHash("tx0"), + 0, + [{ unit: "lovelace", quantity: "5000000" }], + address, + 0, + ) + .txOut(address, [{ unit: "lovelace", quantity: "1000000" }]) + .setDonation("1500000") + .changeAddress(address) + .complete(); + + const cardanoTx = Transaction.fromCbor(TxCBOR(tx)); + expect(cardanoTx.body().donation()).toEqual(BigInt(1500000)); + expect( + calculateOutputLovelaces(tx) + + cardanoTx.body().fee() + + cardanoTx.body().donation()!, + ).toEqual(BigInt(5000000)); + }); + + it("Should fail when inputs cannot cover the donation", async () => { + await expect( + txBuilder + .txIn( + txHash("tx0"), + 0, + [{ unit: "lovelace", quantity: "2000000" }], + address, + 0, + ) + .txOut(address, [{ unit: "lovelace", quantity: "1000000" }]) + .setDonation("1500000") + .changeAddress(address) + .complete(), + ).rejects.toThrow(); + }); + + it("Should reject a non-positive donation", () => { + expect(() => txBuilder.setDonation("0")).toThrow(); + expect(() => txBuilder.setDonation("-1")).toThrow(); + }); + + it("Should throw for the CSL serializer", async () => { + const cslBuilder = new MeshTxBuilder({ serializer: new CSLSerializer() }); + await expect( + cslBuilder + .txIn( + txHash("tx0"), + 0, + [{ unit: "lovelace", quantity: "5000000" }], + address, + 0, + ) + .txOut(address, [{ unit: "lovelace", quantity: "1000000" }]) + .setDonation("1500000") + .changeAddress(address) + .complete(), + ).rejects.toThrow(/donation/i); + }); +});