Multi-Operation Batches
One build = one transaction = one proof. Inside it you can chain any number
of operations on a token, and any number of tokens via repeated .with(...)
blocks. Everything settles atomically.
Snippets assume transfers, account and provider from
Getting Started.
Several operations, one token
Spend a 100n note: 40n to Alice, 30n withdrawn to public, 30n change.
const provingBlockId = (await provider.getBlockNumber()) - 10
const { callAndProof } = await transfers
.build({ autoSetup: true })
.surplusTo(account.address) // absorbs the 30n remainder
.with(tokenAddress, (t) =>
t
.inputs(note100)
.transfer({ recipient: alice, amount: 40n })
.withdraw({ amount: 30n, recipient: account.address }),
)
.execute({ provingBlockId })
const proofDetails = callAndProof.proof.proofFacts?.length
? { proofFacts: callAndProof.proof.proofFacts, proof: callAndProof.proof.data }
: {}
const tx = await account.execute(callAndProof.call, { tip: 0n, ...proofDetails })
await provider.waitForTransaction(tx.transaction_hash)
Several tokens, one transaction
Each token gets its own .with(...) block with its own inputs and outputs.
The per-token balance sheets are independent; surplusTo at the top level
covers all of them.
const { callAndProof } = await transfers
.build({ autoSetup: true, autoDiscover: { notes: "refresh" } })
.surplusTo(account.address)
.with(STRK, (t) => t.inputs(strkNote).transfer({ recipient: alice, amount: 40n }))
.with(ETH, (t) =>
t.inputs(ethNote).withdraw({ amount: 10n, recipient: account.address }),
)
.execute({ provingBlockId })
Things to notice
- Balance-sheet rule holds per token: inputs + deposits must cover
transfers + withdrawals;
surplusTotakes each token's remainder. A per-token override exists too:t.surplusTo(...)inside the block. - At most one
invoke()per transaction - the pool contract enforces a single external call perapply_actions. The builder errors if you chain two. - Larger batches mean larger proofs. Very large recipient lists can hit proof-size limits; wrap the batch in a try/catch and fall back to per-recipient transactions, waiting out the 10-block change-note maturity between them.
- For batches to recipients who may lack channels, do not rely on
autoSetup- pre-flight each recipient instead, as shown on the next page.
Next: Channels & Setup Requirements - knowing what setup a recipient needs.