Evolution SDK
Transactions

Transaction Chaining

Build multiple dependent transactions without waiting for on-chain confirmation between them

Transaction Chaining

Build a sequence of dependent transactions up-front, then submit them in order — no waiting for blocks between steps.

The Problem

Each Cardano transaction spends UTxOs and creates new ones. Normally you can't build the second transaction until the first is confirmed on-chain, because the new UTxOs it creates don't exist yet from the provider's perspective.

This 10–30 second wait between steps is painful for multi-step workflows: batch payouts, batch minting, or any sequence of operations that logically belong together.

How It Works

After .build() completes, the resulting SignBuilder exposes a .chainResult() method that returns:

ChainResult
├── consumed   — UTxOs coin selection spent from the available set
├── available  — remaining unspent UTxOs + newly created outputs (with pre-computed txHash)
└── txHash     — pre-computed hash of this transaction (blake2b-256 of the body)

The available array is the key. It contains the UTxOs your wallet still holds plus any outputs this transaction creates — already tagged with the correct txHash so they're valid as inputs to the next build. Pass it as availableUtxos in the next .build() call.

tx1.build({ availableUtxos: walletUtxos })
     └── tx1.chainResult().available   ← remaining UTxOs + tx1's new outputs


tx2.build({ availableUtxos: tx1.chainResult().available })
     └── tx2.chainResult().available   ← remaining + tx2's new outputs


tx3.build({ availableUtxos: tx2.chainResult().available })

Transactions must be submitted in order. Each transaction spends outputs created by the previous one, so the node will reject tx2 if tx1 hasn't been submitted yet.

Usage

Two sequential payments

The simplest case: two payments built back-to-back, submitted in order.

import { , ,  } from "@evolution-sdk/evolution"

const  = ({
  : "preprod",
  : { : "blockfrost", : "https://cardano-preprod.blockfrost.io/api/v0", : ..! },
  : { : "seed", : ..!, : 0 }
})

const  = .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")
const    = .("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae")

// Build first transaction — auto-fetches wallet UTxOs
const  = await 
  .()
  .({ : , : .(2_000_000n) })
  .()

// Build second transaction immediately — no waiting for tx1 to confirm
const  = await 
  .()
  .({ : , : .(2_000_000n) })
  .({ : .(). })

// Submit in order — tx1 must reach the node before tx2
const  = await .()
await .()

const  = await .()
await .()

Spending an output from the previous transaction

Use tx1.chainResult().available to find the output you want to spend in tx2.

import { , ,  } from "@evolution-sdk/evolution"

const  = ({
  : "preprod",
  : { : "blockfrost", : "https://cardano-preprod.blockfrost.io/api/v0", : ..! },
  : { : "seed", : ..!, : 0 }
})

const  = .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")
const    = .("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae")

// tx1 sends 5 ADA to Alice
const  = await 
  .()
  .({ : , : .(5_000_000n) })
  .()

const  = .()

// Find Alice's output in the chain result — it has a pre-computed txHash
const  = .()
const  = ..(
   => .(.) === 
)!

// tx2 immediately spends Alice's output, forwarding to Bob
const  = await 
  .()
  .({ : [] })
  .({ : , : .(4_500_000n) })
  .({ : . })

// Sign and submit in order
await (await .()).()
await (await .()).()

Three-step batch

Chain three builds together up-front, then submit all three.

import { , ,  } from "@evolution-sdk/evolution"

const  = ({
  : "preprod",
  : { : "blockfrost", : "https://cardano-preprod.blockfrost.io/api/v0", : ..! },
  : { : "seed", : ..!, : 0 }
})

const  = [
  .("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
  .("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"),
  .("addr_test1qpq6xvp5y4fw0wfgxfqmn78qqagkpv4q7qpqyz8s8x3snp5n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgsc3z7t3"),
]

const  = await 
  .()
  .({ : [0], : .(5_000_000n) })
  .()

const  = await 
  .()
  .({ : [1], : .(5_000_000n) })
  .({ : .(). })

const  = await 
  .()
  .({ : [2], : .(5_000_000n) })
  .({ : .(). })

// All three built — now submit in order
for (const  of [, , ]) {
  const  = await .()
  await .()
}

Gotchas

  • Submit in order. Each transaction in the chain depends on outputs from the previous one. Submitting tx2 before tx1 means the node sees inputs that don't exist yet and rejects it.
  • Not retry-safe by default. The chain is built from a single snapshot of chain state. If tx1 fails after you've built tx2 (e.g. a network error mid-submit), you cannot safely retry just tx2 — you need to rebuild the whole chain. See Retry-Safe Transactions for how to structure resilient pipelines.
  • chainResult() is memoized. It's computed once from the build result and cached. Calling it multiple times is free but you always get the same snapshot.
  • The outputs in available are not yet on-chain. They exist only as pre-computed UTxOs. Don't pass them to any provider call (e.g. getUtxos) — they won't be there yet.

Next Steps