Skip to content

Errors and events ​

How to read a failed run's error code and find the step that failed, and how to read what a run hands out: the run event, logs of the template's own, and return data. Error codes lists every code.

Decoding ​

A failed run returns a custom error code. Its low 16 bits are the error kind, and its high 16 bits are a context that locates the failure: for most kinds, the program counter, which is the index of the failing bytecode instruction. Error codes lists the kinds, and the four whose context is something else.

ts
import { decodeBallistaError, explainRunError } from '@jac0xb/ballista';

// A failed `require` at program counter 7: the kind in the low 16 bits, the context in the high 16.
export const decoded = decodeBallistaError((7 << 16) | 6015);
// { code: 464767, kind: 6015, name: 'RequirementFailed', context: 7, source: 'runtime' }

// `compiled` is the template that ran, compiled: here the budgeted payroll from Batch execution.
export const explained = explainRunError((7 << 16) | 6015, compiled)?.message;
// 'RequirementFailed at steps[2] (withinBudget)'
rs
fn decode() {
    use ballista_sdk::decode_ballista_error;

    // A failed `require` at program counter 7: the kind in the low 16 bits, the context in the high 16.
    let decoded = decode_ballista_error((7 << 16) | 6015).unwrap();
    assert_eq!(decoded.name, "RequirementFailed");
    assert_eq!(decoded.context, 7);
}

explainRunError names the step that failed. It uses the compiled template's source map, which records the step and optional label that produced each compiled instruction. For AccountConstraintFailed it names the account instead, and for InvalidRunInputs the input. Give a step a label when its position alone would not make a failure clear, as the budgeted payroll labels its withinBudget check.

Both TypeScript decoders take a bigint as well as a number. Kit's RPC returns a simulation's custom code as a bigint, though its type says number.

Which program failed ​

A called program's error code passes through unchanged, and Anchor programs also count from 6000, so read the logs to tell who failed: the first Program <address> failed: line names it. failedProgram(logs) returns that program, and decodeBallistaFailure and explainRunError decode the code only when it is Ballista's. This simulates a run and says why it would fail:

ts
import {
  compileTransaction,
  getBase64EncodedWireTransaction,
  type Rpc,
  type SimulateTransactionApi,
  type TransactionError,
  type TransactionMessage,
  type TransactionMessageWithFeePayer,
} from '@solana/kit';

import { explainRunError, failedProgram, type CompiledTemplate } from '@jac0xb/ballista';

/** Simulates a message as it stands, unsigned, against the cluster's latest blockhash. */
export async function simulate(
  rpc: Rpc<SimulateTransactionApi>,
  message: TransactionMessage & TransactionMessageWithFeePayer,
) {
  const transaction = getBase64EncodedWireTransaction(compileTransaction(message));
  const { value } = await rpc
    .simulateTransaction(transaction, { encoding: 'base64', replaceRecentBlockhash: true, sigVerify: false })
    .send();
  return value;
}

export type Simulation = Awaited<ReturnType<typeof simulate>>;

/** Why a simulated run fails: the step, if Ballista refused it, or else the program that did. */
export function whyItFails(simulation: Simulation, compiled: CompiledTemplate): string | undefined {
  if (!simulation.err) return undefined;
  const logs = simulation.logs ?? [];
  const code = customCode(simulation.err);
  // Explains the code only if the logs show that Ballista failed, not a program it called.
  const explanation = code === undefined ? undefined : explainRunError(code, compiled, { logs });
  return explanation?.message ?? `${failedProgram(logs) ?? 'The runtime'} refused the transaction`;
}

/** A program's custom error code. Kit's RPC returns it as a `bigint`, though its type says `number`. */
function customCode(error: TransactionError): number | bigint | undefined {
  if (typeof error !== 'object' || !('InstructionError' in error)) return undefined;
  const [, instructionError] = error.InstructionError;
  return typeof instructionError === 'object' && 'Custom' in instructionError ? instructionError.Custom : undefined;
}

A failed run also writes one log line of five numbers: the program counter, the opcode, its two operands and its destination register. That finds the failing instruction in a simulation's logs without decoding the code.

Run events and output ​

Set emitEvent: true and every successful run logs one run event: the template, the rows it processed, and which calls ran (a call skipped by when shows as not run). For values the template computes, use step.emit, which logs a tagged Program data: line, or step.setReturnData. Read them back with parseProgramData and decodeRunEvent, or from the simulation's return data:

ts
import {
  SYSTEM_PROGRAM_ADDRESS_BYTES,
  account,
  data,
  defineTemplate,
  expression,
  step,
  systemTransfer,
} from '@jac0xb/ballista';

/** Sweep everything above `reserve`, and report how much moved. */
export const reportedSweep = defineTemplate({
  emitEvent: true, // log one run event after every successful run
  inputs: { reserve: { type: 'u64' } },
  accounts: {
    systemProgram: { executable: true, address: SYSTEM_PROGRAM_ADDRESS_BYTES },
    vault: { signer: true, writable: true },
    destination: { writable: true },
  },
  steps: [
    step.let(
      'swept',
      expression.subtract(expression.accountField(account.fixed('vault'), 'lamports'), expression.input('reserve')),
    ),
    systemTransfer({
      systemProgram: account.fixed('systemProgram'),
      from: account.fixed('vault'),
      to: account.fixed('destination'),
      lamports: expression.variable('swept'),
    }),
    // A Program data: line of its own, starting with a 4-byte tag.
    step.emit([data.literal(new TextEncoder().encode('SWP1')), data.encode('u64', expression.variable('swept'))]),
    // The bytes the caller gets back.
    step.setReturnData([data.encode('u64', expression.variable('swept'))]),
  ],
});
ts
import { BALLISTA_PROGRAM_ADDRESS, decodeRunEvent, parseProgramData, type RunEvent } from '@jac0xb/ballista';

/** The run events and `emit` outputs Ballista logged. */
export function runOutputs(simulation: Simulation): { events: RunEvent[]; emits: Uint8Array[] } {
  const events: RunEvent[] = [];
  const emits: Uint8Array[] = [];
  // A failed transaction keeps the lines logged before it failed, so read them only on success.
  if (simulation.err) return { events, emits };
  // A `Program data:` line does not name its program; parseProgramData follows the invoke stack.
  for (const line of parseProgramData(simulation.logs ?? [])) {
    if (line.program !== BALLISTA_PROGRAM_ADDRESS) continue;
    for (const field of line.fields) {
      // The run event starts with BEV1, and no template's emit tag can start with BEV.
      const event = decodeRunEvent(field);
      if (event) events.push(event);
      else emits.push(field);
    }
  }
  return { events, emits };
}
ts
import { getBase64Encoder, getU64Decoder } from '@solana/kit';

import { BALLISTA_ADDRESS } from '@jac0xb/ballista/kit';

/**
 * The `u64` a run returned. A transaction's return data is its last instruction's, so the run must
 * come last. The data names the program that set it: this proves Ballista set it, not which
 * template ran.
 */
export function returnedU64(simulation: Simulation): bigint {
  const returned = simulation.returnData;
  if (simulation.err || returned?.programId !== BALLISTA_ADDRESS) {
    throw new Error('The run failed, or Ballista did not set the return data');
  }
  return getU64Decoder().decode(getBase64Encoder().encode(returned.data[0]));
}

Output and Wire format have the details.

BALLISTA / A SMALL MACHINE FOR COMPLEX TRANSACTIONS