Transaction Bakery
This is a light-weight Cardano transaction builder written in Rust.
Our main motivation was to replace the monolithic off-chain tools that are
common in our stacks with modular components, that can be mixed and matched for
your projects specific needs.
How to use
Transaction building can be separated into 3 stages:
- TransactionInfo building (kneading)
- Transaction building (baking)
- Submitting (deliver)
The bakery has tools to do all the 3, but you can also easily delegate some
stages to other components: for example you could build your TransactionInfo
in Haskell, and let the last 2 stages be handled by the bakery.
1. TransactionInfo building (kneading)
In this step we compose a transaction without having to deal with network
related data, costs and fees, etc. The TxScaffold will provide a way to
build a simplified model of a transaction.
For example, a claim transaction from a validator address would look like
the following:
use lbf_demo_plutus_api::demo::plutus::{EqDatum, EqRedeemer};
use num_bigint::BigInt;
use plutus_ledger_api::{
plutus_data::IsPlutusData,
v2::{
datum::{Datum, OutputDatum},
redeemer::Redeemer,
transaction::{TransactionInfo, TransactionInput},
},
};
use std::collections::BTreeMap;
use tx_bakery::{
chain_query::{ChainQuery, FullTransactionOutput},
error::Result,
submitter::Submitter,
tx_info_builder::TxScaffold,
utils::script::ScriptOrRef,
wallet::Wallet,
};
pub fn mk_tx_info(
own_utxos: &BTreeMap<TransactionInput, FullTransactionOutput>,
eq_validator_utxos: &BTreeMap<TransactionInput, FullTransactionOutput>,
eq_redeemer: &EqRedeemer,
eq_datum: &EqDatum,
) -> TransactionInfo {
let fee_input = own_utxos
.iter()
.find(|(_tx_in, tx_out)| tx_out.value.get_ada_amount() >= BigInt::from(5_000_000))
.expect("Cannot find spendable input UTxO.");
let tx_input = eq_validator_utxos
.iter()
.find(|(_, tx_out)| {
if let OutputDatum::InlineDatum(Datum(inline_datum)) = &tx_out.datum
{
EqDatum::from_plutus_data(&inline_datum).unwrap() == *eq_datum
} else {
false
}
})
.expect("UTxO with inline datum not found");
let redeemer = Redeemer(eq_redeemer.to_plutus_data());
TxScaffold::new()
.add_pub_key_input(fee_input.0.clone(), fee_input.1.into())
.add_script_input(tx_input.0.clone(), tx_input.1.into(), None, redeemer)
.build()
}
Note that we didn't even add an output to our transaction. This is OK, as in the
next step, we're going to use ChangeStrategy::Address which instructs the
bakery to send the change to a specified address.
We also don't have to worry about the fees, or the minimum UTxO deposits on
outputs. Those will also be sorted out in the baking stage.
Also note that this function is pure, all side-effects were pushed out from the
core component.
2. Transaction building, signing and submitting (baking and delivering)
Transaction building will convert our TransactionInfo into a valid, CBOR
serialized unsigned transaction.
use lbf_demo_plutus_api::demo::plutus::{EqDatum, EqRedeemer};
use plutus_ledger_api::{
v2::{
address::{Address, Credential},
script::ValidatorHash,
transaction::{TransactionHash},
},
};
use std::collections::BTreeMap;
use tx_bakery::{
chain_query::{ChainQuery, FullTransactionOutput},
error::Result,
submitter::Submitter,
utils::script::ScriptOrRef,
wallet::Wallet,
ChangeStrategy, CollateralStrategy, TxBakery, TxWithCtx,
};
pub async fn build_and_submit(
wallet: &impl Wallet,
chain_query: &impl ChainQuery,
submitter: &impl Submitter,
eq_validator: (ValidatorHash, ScriptOrRef),
eq_redeemer: &EqRedeemer,
eq_datum: &EqDatum,
) -> Result<TransactionHash> {
let eq_validator_addr = Address {
credential: Credential::Script(eq_validator.0.clone()),
staking_credential: None,
};
let own_utxos = chain_query
.query_utxos_by_addr(&wallet.get_change_addr())
.await?;
let eq_validator_utxos = chain_query.query_utxos_by_addr(&eq_validator_addr).await?;
let tx_info = mk_tx_info(&own_utxos, &eq_validator_utxos, eq_redeemer, eq_datum);
let scripts = BTreeMap::from([eq_validator.1.with_script_hash()]);
let collateral = CollateralStrategy::Automatic;
let tx_bakery = TxBakery::init(chain_query).await?;
let change_strategy = ChangeStrategy::Address(wallet.get_change_addr());
let tx = TxWithCtx::new(&tx_info, &scripts, &collateral, &change_strategy);
tx_bakery.bake_and_deliver(submitter, wallet, tx).await
}
The rest of the implementation is about wiring up the actual services,
for example Ogmios as the ChainQuery and Submitter, and a KeyWallet.
To see a full example implementation, please visit
lambda-buffers-for-cardano