Amounts read at run time
A Solana transaction fixes its instruction data when it is signed, so every amount in it must be known in advance. Many useful amounts are not: the balance to sweep, the debt to repay, the part of a deposit to pass on. They exist only when the transaction executes.
A Ballista template can read a number from an account while it runs and pass it to the next call. Getting started builds the simplest case: sweep everything above a reserve. This page shows two more: forward a token balance and split a deposit.
In the examples, step.let computes a value once and gives it a name, and step.require stops the whole transaction unless its condition holds. systemTransfer and tokenTransfer call the System program and the Token program. Each example has a Template tab, the whole template, and a Run tab, the code that builds the instruction to run it, in TypeScript or Rust. Where an example calls another protocol, the code uses marked stand-ins (the System program and its Transfer data) so that it compiles and runs as written; replace them with the protocol's own.
Forward the whole token balance
Move a token account's entire balance to another token account.
import {
TOKEN_PROGRAM_ADDRESS_BYTES,
account,
defineTemplate,
expression,
step,
tokenTransfer,
} from '@jac0xb/ballista';
/** SPL Token account layout: the balance is the u64 at byte 64 of a 165-byte account. */
const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
const TOKEN_ACCOUNT_LENGTH = 165;
/** Move a token account's entire balance, read during the run, to another token account. */
export const forwardTheWholeTokenBalance = defineTemplate({
accounts: {
tokenProgram: { executable: true, address: TOKEN_PROGRAM_ADDRESS_BYTES },
// Token-owned and 165 bytes or more: a token account, or a multisig the transfer refuses.
source: { writable: true, owner: TOKEN_PROGRAM_ADDRESS_BYTES, minDataLength: TOKEN_ACCOUNT_LENGTH },
destination: { writable: true, owner: TOKEN_PROGRAM_ADDRESS_BYTES, minDataLength: TOKEN_ACCOUNT_LENGTH },
authority: { signer: true },
},
steps: [
step.let('balance', expression.accountData(account.fixed('source'), TOKEN_ACCOUNT_AMOUNT_OFFSET, 'u64')),
step.require(expression.greaterThan(expression.variable('balance'), expression.u64(0))),
tokenTransfer({
tokenProgram: account.fixed('tokenProgram'),
source: account.fixed('source'),
destination: account.fixed('destination'),
authority: account.fixed('authority'),
amount: expression.variable('balance'),
}),
],
});import { address, type Address } from '@solana/kit';
import { compileTemplate } from '@jac0xb/ballista';
import { buildKitRunInstruction } from '@jac0xb/ballista/kit';
const TOKEN_PROGRAM = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
/** No inputs: the amount is whatever `source` holds when the run executes. */
export function runForwardTheWholeTokenBalance(run: {
templateAddress: Address;
source: Address;
destination: Address;
authority: Address;
}) {
return buildKitRunInstruction({
compiled: compileTemplate(forwardTheWholeTokenBalance),
templateAddress: run.templateAddress,
accounts: {
tokenProgram: { address: TOKEN_PROGRAM },
source: { address: run.source },
destination: { address: run.destination },
authority: { address: run.authority },
},
});
}/// Move a token account's entire balance, read during the run, to another token account.
pub fn forward_the_whole_token_balance() -> Template {
/// SPL Token account layout: the balance is the u64 at byte 64 of a 165-byte account.
const TOKEN_ACCOUNT_AMOUNT_OFFSET: u32 = 64;
const TOKEN_ACCOUNT_LENGTH: u32 = 165;
Template::new()
.account("tokenProgram", account::program(TOKEN_PROGRAM_ID))
// Token-owned and 165 bytes or more: a token account, or a multisig the transfer refuses.
.account(
"source",
account::writable()
.owner(TOKEN_PROGRAM_ID)
.min_data_length(TOKEN_ACCOUNT_LENGTH),
)
.account(
"destination",
account::writable()
.owner(TOKEN_PROGRAM_ID)
.min_data_length(TOKEN_ACCOUNT_LENGTH),
)
.account("authority", account::signer())
.step(step::let_(
"balance",
account_data("source", TOKEN_ACCOUNT_AMOUNT_OFFSET, ReadType::U64),
))
.step(step::require(var("balance").gt(u64(0))))
.step(token_transfer(
"tokenProgram",
"source",
"destination",
"authority",
var("balance"),
))
}/// No inputs: the amount is whatever `source` holds when the run executes.
pub fn forward_the_whole_token_balance(
template: Pubkey,
source: Pubkey,
destination: Pubkey,
authority: Pubkey,
) -> RunResult {
let instruction = templates::forward_the_whole_token_balance()
.compile()?
.run(template)
.account("tokenProgram", TOKEN_PROGRAM_ID)
.account("source", source)
.account("destination", destination)
.account("authority", authority)
.instruction()?;
Ok(instruction)
}accountData reads a value of the given type at a byte offset in an account's data. An SPL Token account stores its balance as a u64 at offset 64, so the template reads it there. It requires both accounts to be owned by the Token program and to hold at least 165 bytes. Those pins don't prove a token account: a 355-byte Token multisig passes them too, though the transfer from or to one then fails. See what a pin proves. The require stops the run when the balance is zero.
Finding a field's offset
An offset is the number of bytes before the field in the account's data. Add up the sizes of the fields that come before it in the program's account struct: an SPL Token account starts with a 32-byte mint and a 32-byte owner, so its u64 amount starts at byte 64. Anchor programs put an 8-byte discriminator first, so their first field is at byte 8. An Anchor IDL lists each account's fields in order with their types, which is enough to add up the sizes, as long as every field before the one you want has a fixed size.
Split what arrived
Send a percentage of a vault's balance above a reserve to a partner, and the rest to a treasury. The balance keeps changing as deposits arrive, so the split is computed when the transaction executes.
import {
SYSTEM_PROGRAM_ADDRESS_BYTES,
account,
defineTemplate,
expression,
step,
systemTransfer,
} from '@jac0xb/ballista';
/** Pay a partner `shareBps` of the vault's balance above `reserve`, and the treasury the rest. */
export const splitWhatArrived = defineTemplate({
inputs: { reserve: { type: 'u64' }, shareBps: { type: 'u64' } },
accounts: {
systemProgram: { executable: true, address: SYSTEM_PROGRAM_ADDRESS_BYTES },
vault: { signer: true, writable: true },
partner: { writable: true },
treasury: { writable: true },
},
steps: [
step.let(
'distributable',
expression.subtract(expression.accountField(account.fixed('vault'), 'lamports'), expression.input('reserve')),
),
step.let(
'partnerShare',
expression.divide(
expression.multiply(expression.variable('distributable'), expression.input('shareBps')),
expression.u64(10_000),
),
),
systemTransfer({
systemProgram: account.fixed('systemProgram'),
from: account.fixed('vault'),
to: account.fixed('partner'),
lamports: expression.variable('partnerShare'),
}),
systemTransfer({
systemProgram: account.fixed('systemProgram'),
from: account.fixed('vault'),
to: account.fixed('treasury'),
lamports: expression.subtract(expression.variable('distributable'), expression.variable('partnerShare')),
}),
],
});import type { Address } from '@solana/kit';
import { compileTemplate } from '@jac0xb/ballista';
import { SYSTEM_PROGRAM_ADDRESS, buildKitRunInstruction } from '@jac0xb/ballista/kit';
export function runSplitWhatArrived(run: {
templateAddress: Address;
vault: Address;
partner: Address;
treasury: Address;
reserve: bigint;
shareBps: bigint;
}) {
return buildKitRunInstruction({
compiled: compileTemplate(splitWhatArrived),
templateAddress: run.templateAddress,
inputs: { reserve: run.reserve, shareBps: run.shareBps },
accounts: {
systemProgram: { address: SYSTEM_PROGRAM_ADDRESS },
vault: { address: run.vault },
partner: { address: run.partner },
treasury: { address: run.treasury },
},
});
}/// Pay a partner `shareBps` of the vault's balance above `reserve`, and the treasury the rest.
pub fn split_what_arrived() -> Template {
Template::new()
.input("reserve", Type::U64)
.input("shareBps", Type::U64)
.account("systemProgram", account::program(SYSTEM_PROGRAM_ID))
.account("vault", account::signer().writable())
.account("partner", account::writable())
.account("treasury", account::writable())
.step(step::let_(
"distributable",
lamports("vault") - input("reserve"),
))
.step(step::let_(
"partnerShare",
var("distributable") * input("shareBps") / u64(10_000),
))
.step(system_transfer(
"systemProgram",
"vault",
"partner",
var("partnerShare"),
))
.step(system_transfer(
"systemProgram",
"vault",
"treasury",
var("distributable") - var("partnerShare"),
))
}pub fn split_what_arrived(
template: Pubkey,
vault: Pubkey,
partner: Pubkey,
treasury: Pubkey,
reserve: u64,
share_bps: u64,
) -> RunResult {
let instruction = templates::split_what_arrived()
.compile()?
.run(template)
.input("reserve", reserve)
.input("shareBps", share_bps)
.account("systemProgram", SYSTEM_PROGRAM_ID)
.account("vault", vault)
.account("partner", partner)
.account("treasury", treasury)
.instruction()?;
Ok(instruction)
}shareBps is the partner's share in basis points (hundredths of a percent, so 10,000 is 100%). Integer division rounds the partner's share down. The treasury receives the remainder rather than a second percentage, so rounding never leaves a lamport of the distributable amount behind.