Skip to content

Building Transactions (Rust)

This guide walks the full life of a transaction: describe it in TxPlan YAML, build it offline, sign it with the right keys, and submit it with your own HTTP client. The YAML shapes for every intent — staking, governance, pools, minting, Plutus — are cataloged in the TxPlan reference; this page shows how to drive them from Rust.

Every transaction follows the same four steps (providers need --features providers):

use ccl::{Bridge, Network};
use ccl::providers::YaciProvider;
let bridge = Bridge::new()?;
let provider = YaciProvider::default(); // or BlockfrostProvider, or your own impl
// 1. Describe — TxPlan YAML (see the intent catalog)
let yaml = format!(r#"
version: 1.0
transaction:
- tx:
from: {sender}
intents:
- type: payment
address: {receiver}
amounts:
- unit: lovelace
quantity: "5000000"
"#);
// 2. Build — offline; UTXO selection, fee, and change happen in the native lib
let result = bridge.quicktx().build_with(&yaml, &provider, &sender, 0, None)?;
// (or bridge.quicktx().build(&yaml, &utxos, &protocol_params, None, additional_signers) with your own chain data)
// 3. Sign — with the key roles the transaction's certificates require
let signed = bridge.account().sign_tx(&mnemonic, Network::Testnet, 0, 0, &result.tx_cbor)?;
// 4. Submit — any Blockfrost-compatible endpoint; the library never submits
// e.g. with ureq: POST {url}/tx/submit, Content-Type: application/cbor, body = hex-decoded `signed`

sign_tx witnesses with the payment key only. Certificates need their own witness — use sign_tx_with_keys with roles in order:

Transaction containskeys
Payments, metadata, minting, Plutus operations&["payment"] (or plain sign_tx)
stake_registration / stake_deregistration / stake_delegation / stake_withdrawal / voting_delegation&["payment", "stake"]
drep_registration / drep_update / drep_deregistration / voting&["payment", "drep"]
governance_proposal&["payment"]
pool_registration / pool_update / pool_retirement&["payment", "stake"] when the pool is keyed to the account’s stake key

A missing witness is rejected by the node with MissingVKeyWitnessesUTXOW. The same table gives the fee’s witness budget: pass additional_signers = len(keys) - 1 to the build (the input UTXOs already cover the payment key). For a native-script spend whose only inputs sit at the script address, pass the number of the script’s sig keys instead.

Worked example: register and delegate stake

Section titled “Worked example: register and delegate stake”

Two transactions — the registration must be on-chain before the delegation:

let stake_yaml = format!(r#"
version: 1.0
transaction:
- tx:
from: {sender}
intents:
- type: stake_registration
stake_address: {stake_address}
"#);
let reg = bridge.quicktx().build_with(&stake_yaml, &provider, &sender, 1, None)?;
let signed_reg = bridge.account().sign_tx_with_keys(
&mnemonic, Network::Testnet, 0, 0, &reg.tx_cbor, &["payment", "stake"])?;
// submit signed_reg; wait for inclusion before the next step
let deleg_yaml = format!(r#"
version: 1.0
transaction:
- tx:
from: {sender}
intents:
- type: stake_delegation
stake_address: {stake_address}
pool_id: pool1...
"#);
let deleg = bridge.quicktx().build_with(&deleg_yaml, &provider, &sender, 1, None)?;
let signed_deleg = bridge.account().sign_tx_with_keys(
&mnemonic, Network::Testnet, 0, 0, &deleg.tx_cbor, &["payment", "stake"])?;

Worked example: DRep registration, then vote

Section titled “Worked example: DRep registration, then vote”

The DRep credential comes from the governance API:

let drep: serde_json::Value =
serde_json::from_str(&bridge.gov().drep_key_from_mnemonic(&mnemonic, Network::Testnet, 0)?)?;
let credential = drep["verification_key_hash"].as_str().unwrap();
let drep_yaml = format!(r#"
version: 1.0
transaction:
- tx:
from: {sender}
intents:
- type: drep_registration
drep_credential_hex: {credential}
drep_credential_type: key_hash
anchor_url: https://example.com/meta.json
anchor_hash: {anchor_hash}
"#);
let reg = bridge.quicktx().build_with(&drep_yaml, &provider, &sender, 1, None)?;
let signed = bridge.account().sign_tx_with_keys(
&mnemonic, Network::Testnet, 0, 0, &reg.tx_cbor, &["payment", "drep"])?;

To vote on a governance action, the action id is the proposal transaction’s hash plus its index (a proposal you submit yourself returns its hash from buildresult.tx_hash). Sign the voting transaction with &["payment", "drep"].

Worked example: mint under a native script

Section titled “Worked example: mint under a native script”
let mint_yaml = format!(r#"
version: 1.0
transaction:
- tx:
from: {sender}
intents:
- type: minting
assets:
- name: TestNFT
value: 1
receiver: {receiver}
script_hex: "820180"
script_type: 0
"#);
let mint = bridge.quicktx().build_with(&mint_yaml, &provider, &sender, 0, None)?;
let signed = bridge.account().sign_tx(&mnemonic, Network::Testnet, 0, 0, &mint.tx_cbor)?;

An empty ScriptAll policy (820180) needs no extra signature; a sig-keyed policy needs the corresponding key’s witness.

By default execution units are computed offline (embedded Scalus evaluator) — a Plutus transaction is a normal build:

let result = bridge.quicktx().build_with(&plutus_mint_yaml, &provider, &sender, 0, None)?;

To cost against a real node instead, pass an evaluator — build_with then runs the two-pass flow (draft → remote evaluate → rebuild):

use ccl::providers::BlockfrostEvaluator;
let evaluator = BlockfrostEvaluator::new(&project_id, "preprod")?;
let result = bridge.quicktx().build_with(&plutus_mint_yaml, &provider, &sender, 0, Some(&evaluator))?;

Or supply units yourself with the offline build:

use serde_json::json;
let result = bridge.quicktx().build(&plutus_mint_yaml, &utxos, &params, 0,
Some(&json!([{"mem": 2000000, "steps": 500000000}])))?;

For spending a script UTXO (script_collect_from), supply the locked UTXO (with its data_hash) plus a separate UTXO for fee/collateral in utxos — see the catalog entry and the end-to-end lock-then-spend flow in wrappers/rust/tests/quicktx_integration_test.rs.

  • CCL Error -10 (CCL_ERROR_TX_BUILD) — the plan didn’t build: malformed YAML, wrong intent field, or a Plutus costing problem. Compare against the catalog.
  • CCL Error -8 (CCL_ERROR_INSUFFICIENT_FUNDS) — the supplied UTXOs can’t cover outputs + fee.
  • Node rejection MissingVKeyWitnessesUTXOW — a certificate wasn’t witnessed; check the roles table above.