ChainContext¶
The chain‑side inputs to a deterministic transaction build, captured as a single value.
Why¶
Every Cardano tx build needs five things from the outside world:
- Protocol parameters at a moment in time (fee coefficients, cost models, collateral percentage).
- Resolved UTxOs for every input the tx refers to (wallet, treasury, reference scripts).
- The network id and tip slot for that same ledger view.
- A script evaluator that runs Plutus redeemers against a
draft tx so the builder can patch in the right
ExUnits. - A phase-1 validation context for the final unsigned tx.
Bundling these into a record makes the dependency on "reality" explicit, replaceable, and testable.
data ChainContext = ChainContext
{ ccNetwork :: Network
, ccTipSlot :: SlotNo
, ccPParams :: PParams ConwayEra
, ccUtxos :: Map TxIn (TxOut ConwayEra)
, ccEvaluateTx :: ConwayTx -> IO (EvaluateTxResult ConwayEra)
}
runFromIntent and the action-specific builders accept a
ChainContext and nothing else from the live chain. Whichever way
the context was populated — live node, snapshot, mocked evaluator —
the build is the same code path.
Live mode¶
The release-facing CLI paths use withLiveContext. It acquires one
local-node N2C query handle, samples protocol parameters, required
UTxOs, tip slot, and network, then keeps the build and evaluator calls
inside the acquired callback. That matters because the builder can call
the evaluator more than once while converging fees and ExUnits.
import Amaru.Treasury.ChainContext (withLiveContext)
withLiveContext network provider (Set.fromList allRequiredTxIns) $ \ctx ->
runFromIntent ctx intent
liveContext still exists for one-shot callers, but production builds
prefer withLiveContext so protocol parameters, UTxOs, tip slot, and
script evaluation all come from one sampled view.
After building and fee alignment, the action runners call
Cardano.Tx.Validate.validatePhase1 through
Build.Common.validateFinalPhase1. Missing vkey witnesses are expected
for an unsigned transaction and are filtered out; remaining ledger
failures abort the build before CBOR is written.
Frozen mode¶
Same record, populated from fixtures:
pparams— a JSON snapshot recorded once viacardano-cli query protocol-parameters.- UTxOs — a small JSON / CBOR pack carrying value and reference
script for each
TxInthe build will reference. - network and tip slot — fixed values used by the final phase-1 preflight.
- evaluator — usually a
pure (Right knownExUnits)map keyed by redeemer purpose, captured during a previous live run.
import Amaru.Treasury.ChainContext (frozenContext)
let ctx = frozenContext frozenPParams frozenUtxos pureEvaluator
Use frozenContextAt when the test needs to pin a specific network or
slot instead of the default mainnet slot 0.
A frozen ChainContext makes the build immune to:
- The original wallet / treasury inputs being spent.
- Pparams drifting at an epoch boundary.
- Hard forks or governance-driven cost-model changes.
- Network outages.
The same runFromIntent path produces byte-identical CBOR
regardless.
Where it sits in the stack¶
┌─────────────────────────────────────────────┐
│ amaru-treasury-tx tx-build │
└──────────────────┬──────────────────────────┘
│
▼
┌───────────────────────┐
│ ChainContext │ ← reality (live | frozen)
└───────────────────────┘
│
▼
┌───────────────────────┐
│ Build │ ← dispatcher: translate,
│ runFromIntent │ build, fee-align,
└───────────────────────┘ re-evaluate, phase-1 validate
│
▼
┌───────────────────────┐
│ Tx.Swap │ ← pure TxBuild q e () program
│ swapProgram │ (DSL, no IO)
└───────────────────────┘
The split is enforced by types: pure transaction programs cannot do
IO, Build cannot read pparams or query UTxOs except
through the context, and the CLI or golden harness chooses whether
that context is live or frozen.