Skip to content

Building Transactions (Go)

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 Go.

Every transaction follows the same four steps:

bridge, err := ccl.New()
if err != nil { log.Fatal(err) }
defer bridge.Close()
provider := ccl.NewYaciProvider("") // or a BlockfrostProvider, or your own
// 1. Describe — TxPlan YAML (see the intent catalog)
yaml := fmt.Sprintf(`
version: 1.0
transaction:
- tx:
from: %s
intents:
- type: payment
address: %s
amounts:
- unit: lovelace
quantity: "5000000"
`, sender, receiver)
// 2. Build — offline; UTXO selection, fee, and change happen in the native lib
result, err := bridge.QuickTx.BuildWith(yaml, provider, sender, 0)
// (or bridge.QuickTx.Build(yaml, utxos, protocolParams, additionalSigners) with your own chain data)
// 3. Sign — with the key roles the transaction's certificates require
signed, err := bridge.Account.SignTx(mnemonic, ccl.Testnet, 0, 0, result.TxCbor)
// 4. Submit — any Blockfrost-compatible endpoint; the library never submits
txBytes, _ := hex.DecodeString(signed)
resp, err := http.Post(submitURL+"/tx/submit", "application/cbor", bytes.NewReader(txBytes))

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

Transaction containsRoles
Payments, metadata, minting, Plutus operations"payment" (or plain SignTx)
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:

stakeYaml := fmt.Sprintf(`
version: 1.0
transaction:
- tx:
from: %s
intents:
- type: stake_registration
stake_address: %s
`, sender, account.StakeAddress)
reg, err := bridge.QuickTx.BuildWith(stakeYaml, provider, sender, 1)
signedReg, err := bridge.Account.SignTxWithKeys(mnemonic, ccl.Testnet, 0, 0, reg.TxCbor, "payment", "stake")
// submit signedReg; wait for inclusion before the next step
delegYaml := fmt.Sprintf(`
version: 1.0
transaction:
- tx:
from: %s
intents:
- type: stake_delegation
stake_address: %s
pool_id: pool1...
`, sender, account.StakeAddress)
deleg, err := bridge.QuickTx.BuildWith(delegYaml, provider, sender, 1)
signedDeleg, err := bridge.Account.SignTxWithKeys(mnemonic, ccl.Testnet, 0, 0, deleg.TxCbor, "payment", "stake")

Worked example: DRep registration, then vote

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

The DRep credential comes from the governance API:

drep, err := bridge.Gov.DrepKeyFromMnemonic(mnemonic, ccl.Testnet, 0)
drepYaml := fmt.Sprintf(`
version: 1.0
transaction:
- tx:
from: %s
intents:
- type: drep_registration
drep_credential_hex: %s
drep_credential_type: key_hash
anchor_url: https://example.com/meta.json
anchor_hash: %s
`, sender, drep.VerificationKeyHash, anchorHash)
reg, err := bridge.QuickTx.BuildWith(drepYaml, provider, sender, 1)
signedReg, err := bridge.Account.SignTxWithKeys(mnemonic, ccl.Testnet, 0, 0, reg.TxCbor, "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.TxHash). Sign the voting transaction with "payment", "drep".

Worked example: mint under a native script

Section titled “Worked example: mint under a native script”
mintYaml := fmt.Sprintf(`
version: 1.0
transaction:
- tx:
from: %s
intents:
- type: minting
assets:
- name: TestNFT
value: 1
receiver: %s
script_hex: "820180"
script_type: 0
`, sender, receiver)
mint, err := bridge.QuickTx.BuildWith(mintYaml, provider, sender, 0)
signedMint, err := bridge.Account.SignTx(mnemonic, ccl.Testnet, 0, 0, mint.TxCbor)

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:

result, err := bridge.QuickTx.BuildWith(plutusMintYaml, provider, sender, 0)

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

evaluator, _ := ccl.NewBlockfrostEvaluator(projectID, "preprod")
result, err := bridge.QuickTx.BuildWith(plutusMintYaml, provider, sender, 0, evaluator)

Or supply units yourself with the offline Build:

result, err := bridge.QuickTx.Build(plutusMintYaml, utxos, params, 0,
[]map[string]interface{}{{"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/go/ccl/script_spend_test.go.

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