How it works
A template is three things: the inputs a caller supplies, the accounts it works with, and the steps it runs. You write it once and upload it. After that, anyone can run it with new inputs and accounts, and every run applies the same checks.
A template, in one screen
This one pays up to eight people from a treasury, each a different amount, and refuses to go below a reserve.
const template = defineTemplate({
// Inputs: values the caller supplies with each run.
inputs: { reserve: { type: 'u64' } },
// Accounts: what each account must be. A run fails if one does not match.
accounts: {
systemProgram: { executable: true, address: SYSTEM_PROGRAM_ADDRESS_BYTES },
treasury: { signer: true, writable: true },
},
// Batch: a row of accounts (and inputs) repeated once per recipient.
batch: {
maxIterations: 8,
row: { recipient: { writable: true } },
rowInputs: { amount: { type: 'u64' } },
},
// Steps: run in order, top to bottom.
steps: [
step.forEach([
systemTransfer({
systemProgram: account.fixed('systemProgram'),
from: account.fixed('treasury'),
to: account.iteration('recipient'),
lamports: expression.rowInput('amount'),
}),
]),
step.require(
expression.greaterThanOrEqual(
expression.accountField(account.fixed('treasury'), 'lamports'),
expression.input('reserve'),
),
'keepsReserve',
),
],
});Inputs
Named, typed values the caller sends with each run: bool, u64, i64, u128, pubkey or bytes. Row inputs are sent once per batch row, so each recipient can get a different amount.
Accounts
Each declared account states what it must be: a signer, writable, a program (executable), a fixed address, a fixed owner, a minimum data length. The caller supplies the real accounts, and the run fails if one does not match.
Steps name accounts in two ways:
| Reference | Means |
|---|---|
account.fixed('treasury') | An account from accounts, the same for the whole run |
account.iteration('recipient') | The current row's account, inside forEach only |
Some templates also take an account group: a list of accounts, sized by the caller, passed along to one call without being read.
Steps
Steps run in order, top to bottom:
step.letcomputes a value and names it, andstep.requirefails the whole run unless a condition holds.step.invokecalls another program, and awhencondition on it skips just that call. Helpers such assystemTransferbuild one for you.step.forEachruns its steps once per batch row, andstep.repeata counted number of times. A count above the loop'smaxfails the run withLoopCountExceeded; it isn't cut down tomax.- Other steps log events, set return data, and write registry fields.
The template language lists every step and its rules.
Expressions
Steps compute with expressions. An expression can read:
- an input (
expression.input,expression.rowInput); - an account's address (
expression.accountKey), owner, lamports or data length (expression.accountField); - a field of a registry entry (
expression.registry); - a number or address at a byte offset in an account's data (
expression.accountData); - the clock, a derived PDA, or data returned by the last call;
- the other instructions in the transaction, through the Instructions sysvar (
expression.instructionCount,expression.instructionDataand others); - a byte range of a read-only account, without copying it.
It can combine them with checked arithmetic (add, subtract, multiply, divide, remainder, min, max), exact multiplyDivide that rounds down or up, powerOfTen, shifts and bitwise operations, comparisons, and/or/not, and select. Overflow fails the run instead of wrapping. Inputs and expressions covers the math, including prices and decimals. The language reference covers reading other instructions and byte ranges, and lists every source.
Upload: checked, then locked
When you upload a template, the Ballista program checks all of it once; Trust model lists what it checks. A template that passes is finalized: locked for good. It cannot be changed or closed, and a new version gets a new template ID. See Template lifecycle.
Run: all or nothing
A run checks the caller's accounts and inputs against the declarations, then works through the steps in order. If any check or call fails, the whole Solana transaction is undone, including calls that had already succeeded.
A template has no authority of its own: see when Ballista signs. It keeps nothing between runs except in the registry entries it declares.
For how a template is stored as bytes, see Wire format.