Skip to main content

TransactionBuilder

In order to simplify transaction creation, we provide a TransactionBuilder struct that manages witnesses, fee calculation, change addresses and such. Assume we have instantiated an instance under the variable builder for this explanation. The TransactionBuilder requires several protocol parameters governing Cardano to be created which is shown in the following section. These are specified initially in the genesis file for Cardano nodes.

The minimum required for a valid transaction is to add inputs, outputs, and either set the fee explicitly with builder.set_fee(fee), or calculate it implicitly using builder.add_change_if_needed(address). Optionally a transaction can also have certificates, reward withdrawals, metadata, and minting added to it. Any change made to the builder can impact the size and thus the fee so the fee should be the last thing set. If implicitly setting the fee any extra ADA (inputs + withdrawals - outputs + refund - deposit - min fee) is sent to the provided change address. Fees must be sufficient, i.e. inputs + withdrawals + refund >= outputs + deposit + fee which must be manually ensured if you explicitly set the fee. Any extra fee is not necessary and the extra ADA beyond that will be burned. Once the transaction is ready, const body = builder.build() can be called to return a ready TransactionBody.

Withdrawals are ADA withdrawn as part of the rewards generated by staking and deposits are refundable ADA locked while resources such as stake certificates or pool registrations exist on the blockchain. They are returned as refunds when these resources are deregistered/retired.

To get to a transaction ready to post on the blockchain, we must create a Transaction from that, which consists of the TransactionBody, a matching TransactionWitnessSet and optionally an AuxiliaryData. The witnesses and optional metadata must match those provided to the builder. The witnesses must sign the hash of the transaction body returned by hash_transaction(body). In addition to the witnesses for inputs, withdrawals and some certificates require witnesses as well. For example, staking address registration does not require a witness while stake address de-registration requires one. For any questions or doubts about the rules governing fees, deposits, rewards, certificates or which witness types are required refer to the specs for the relevant era, specifically the Shelley design specification for general design for non-governance certificates. Refer to the Conway specs for those. The formal specification could be useful for specific details as well. The design spec contains details about which certificates require which type of witnesses in the Certificates and Registrations section.

TransactionBuilderConfig

To correctly make transactions the builder must know some on-chain parameters such as the current fee costs, key deposits, etc. These can all potentially change, even if some have largely been static for large periods of time. We pass these into the builder via the TransactionBuilderConfigBuilder. For test things out hard-coding them might suffice, but these parameters should ideally be fetched from the current blockchain head or your transactions could fail to be accepted by the network or will end up paying higher fees. The cost models parameter is optional if you are not building a transaction that utilizes Plutus smart contracts.

Code examples for the builders will assume you have a make_tx_builder() function that creates a TransactionBuilder with the appropriate config.

Blockfrost

One way of getting this information is via the epochs/latest/parameters endpoint of blockfrost. This can be automated from rust using the cml-blockfrost crate's make_tx_builder_cfg(). Blockfrost is by no means necessary but it can be convenient. It is possible to get this information by other means as well e.g. having a synced cardano node.

Using cml-blockfrost (rust):

let cfg = cml_blockfrost::make_tx_builder_cfg(&api).await.unwrap();
let mut tx_builder = TransactionBuilder::new(cfg);

This could also be done manually similar to below (or reference cml-blockfrost's code)

Manually using WASM:

let params = await blockfrost.epochsLatestParameters();

// cost order is based on lex ordering of keys
let costModels = CML.CostModels.new();
let v1Costs = params.cost_models['PlutusV1'];
if (v1Costs != null) {
let v1CMLCosts = CML.IntList.new();
for (key in Object.keys(v1Costs).toSorted()) {
v1CMLCosts.add(CML.Int.new(v1Costs[key]));
}
costModels.set_plutus_v1(v1CMLCosts);
}
// cost order is based on lex ordering of keys
let v2Costs = params.cost_models['PlutusV2'];
if (v2Costs != null) {
let v2CMLCosts = CML.IntList.new();
for (key in Object.keys(v2Costs).toSorted()) {
v2CMLCosts.add(CML.Int.new(v2Costs[key]));
}
costModels.set_plutus_v2(v2CMLCosts);
}
// note: as of writing this the sancho testnet format is different for v3
// compared to v1/v2. this may remain true once mainnet switches over so
// please inspect the object you are getting for cost models from blockfrost

let configBuilder = CML.TransactionBuilderConfigBuilder.new()
.fee_algo(CML.LinearFee.new(params.min_fee_a, params.min_fee_b))
.coins_per_utxo_byte(BigNum(params.coins_per_utxo_size))
.pool_deposit(BigNum(params.pool_deposit))
.key_deposit(BigNum(params.key_deposit))
.max_value_size(Number(params.max_val_size))
.max_tx_size(params.max_tx_size)
.ex_unit_prices(CML.ExUnitPrices.new(
CML.SubCoin.from_base10_f32(params.price_mem),
CML.SubCoin.from_base10_f32(params.price_step)
))
.cost_models(costModels)
.collateral_percentage(params.collateral_percent)
max_collateral_inputs(params.max_collateral_inputs);
let mut txBuilder = CML.TransactionBuilder.new(configBuilder.build());