Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

Common patterns and best practices for working with CallData in production applications.

Function Call Encoding

Single Function Calls

import { Abi, Address, TokenBalance } from '@tevm/voltaire';

const abi = Abi([{
  name: "transfer",
  type: "function",
  inputs: [
    { name: "to", type: "address" },
    { name: "amount", type: "uint256" }
  ]
}]);

// Property-based encoding (recommended)
const calldata = abi.transfer.encode(
  Address("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
  TokenBalance.fromUnits("1", 18)
);
Benefits:
  • Type-safe parameters
  • IDE autocomplete
  • Compile-time validation

Dynamic Parameters

import { Abi, Address } from '@tevm/voltaire';

const abi = Abi([{
  name: "batchTransfer",
  type: "function",
  inputs: [
    { name: "recipients", type: "address[]" },
    { name: "amounts", type: "uint256[]" }
  ]
}]);

const recipients = [
  Address("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
  Address("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"),
];

const amounts = [
  TokenBalance.fromUnits("1", 18),
  TokenBalance.fromUnits("2", 18),
];

const calldata = abi.batchTransfer.encode(recipients, amounts);

Function Routing

Selector Matching

import { CallData } from '@tevm/voltaire';

const ERC20_SELECTORS = {
  TRANSFER: "0xa9059cbb",
  APPROVE: "0x095ea7b3",
  TRANSFER_FROM: "0x23b872dd",
  BALANCE_OF: "0x70a08231",
};

function routeERC20Call(calldata: CallData) {
  const selector = CallData.getSelector(calldata);
  const selectorHex = Hex.fromBytes(selector);

  switch (selectorHex) {
    case ERC20_SELECTORS.TRANSFER:
      return handleTransfer(calldata);
    case ERC20_SELECTORS.APPROVE:
      return handleApprove(calldata);
    case ERC20_SELECTORS.TRANSFER_FROM:
      return handleTransferFrom(calldata);
    case ERC20_SELECTORS.BALANCE_OF:
      return handleBalanceOf(calldata);
    default:
      throw new Error(`Unknown function: ${selectorHex}`);
  }
}

Decoding by Selector

import { CallData, Abi } from '@tevm/voltaire';

const ERC20_ABI = Abi([
  {
    name: "transfer",
    type: "function",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" }
    ]
  },
  {
    name: "approve",
    type: "function",
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" }
    ]
  }
]);

function decodeERC20Call(calldata: CallData) {
  const decoded = CallData.decode(calldata, ERC20_ABI);

  switch (decoded.signature) {
    case "transfer(address,uint256)": {
      const [to, amount] = decoded.parameters;
      return { type: "transfer", to, amount };
    }
    case "approve(address,uint256)": {
      const [spender, amount] = decoded.parameters;
      return { type: "approve", spender, amount };
    }
    default:
      throw new Error(`Unknown function: ${decoded.signature}`);
  }
}

Multicall Patterns

Batch Encoding

import { CallData, Abi } from '@tevm/voltaire';

const multicallAbi = Abi([{
  name: "multicall",
  type: "function",
  inputs: [
    { name: "calls", type: "bytes[]" }
  ]
}]);

// Encode individual calls
const call1 = erc20Abi.balanceOf.encode(
  Address("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
);

const call2 = erc20Abi.balanceOf.encode(
  Address("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC")
);

const call3 = erc20Abi.transfer.encode(
  Address("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
  TokenBalance.fromUnits("100", 18)
);

// Batch into multicall
const multicallData = multicallAbi.multicall.encode([call1, call2, call3]);

Decoding Multicall Results

function decodeMulticallResults(
  results: Uint8Array[],
  expectedTypes: string[]
): unknown[] {
  return results.map((result, i) => {
    const abi = Abi([{
      name: "result",
      type: "function",
      outputs: [{ name: "value", type: expectedTypes[i] }]
    }]);

    const decoded = CallData.decode(result, abi);
    return decoded.parameters[0];
  });
}

// Usage
const results = await multicall(multicallData);
const [balance1, balance2, transferSuccess] = decodeMulticallResults(
  results,
  ["uint256", "uint256", "bool"]
);

Gas Optimization

Zero Byte Optimization

CallData costs 4 gas per zero byte, 16 gas per non-zero byte:
// Expensive (padding creates non-zero bytes)
const amount = Uint256.from(1n);
// Encoded: 0x0000000000000000000000000000000000000000000000000000000000000001

// Consider using smaller types when possible
const abi = Abi([{
  name: "transfer",
  type: "function",
  inputs: [
    { name: "to", type: "address" },
    { name: "amount", type: "uint96" }  // Smaller type
  ]
}]);

Calldata Compression

// Pack multiple values into single uint256
function packAddressAndAmount(addr: Address, amount: bigint): Uint256 {
  // Address (20 bytes) + Amount (12 bytes) = 32 bytes
  const addrBigint = BigInt(addr.toHex());
  const packed = (addrBigint << 96n) | (amount & ((1n << 96n) - 1n));
  return Uint256.from(packed);
}

// Unpack on-chain
// address addr = address(uint160(packed >> 96));
// uint96 amount = uint96(packed);

Error Handling

Validation Before Encoding

import { CallData, Address, TokenBalance } from '@tevm/voltaire';

function encodeTransfer(
  to: string,
  amount: string,
  decimals: number
): CallData {
  // Validate inputs
  if (!Address.isValid(to)) {
    throw new Error(`Invalid address: ${to}`);
  }

  const recipient = Address(to);

  try {
    const tokenAmount = TokenBalance.fromUnits(amount, decimals);
    return abi.transfer.encode(recipient, tokenAmount);
  } catch (error) {
    throw new Error(`Invalid amount: ${amount}`);
  }
}

Decoding with Fallback

function safeDecodeCallData(
  calldata: CallData,
  abi: Abi
): CallDataDecoded | null {
  try {
    return CallData.decode(calldata, abi);
  } catch (error) {
    console.warn("Failed to decode calldata:", error);
    return null;
  }
}

// Usage with fallback
const decoded = safeDecodeCallData(calldata, abi);
if (decoded) {
  console.log("Function:", decoded.signature);
  console.log("Parameters:", decoded.parameters);
} else {
  console.log("Raw selector:", CallData.getSelector(calldata));
}

Transaction Construction

Complete Flow

import {
  Transaction, CallData, Address, Abi,
  TokenBalance, Nonce, GasLimit, Wei, Gwei, ChainId
} from '@tevm/voltaire';

async function createERC20Transfer(
  token: Address,
  to: Address,
  amount: TokenBalance,
  signer: Signer
): Promise<Transaction> {
  // Encode transfer call
  const abi = Abi([{
    name: "transfer",
    type: "function",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" }
    ]
  }]);

  const calldata: CallData = abi.transfer.encode(to, amount);

  // Create transaction
  const tx = Transaction({
    type: Transaction.Type.EIP1559,
    to: token,
    value: Wei(0),
    chainId: ChainId(1),
    nonce: await signer.getNonce(),
    maxFeePerGas: Gwei(30),
    maxPriorityFeePerGas: Gwei(2),
    gasLimit: GasLimit(100000),
    data: calldata,
  });

  // Sign and return
  return signer.sign(tx);
}

Testing Patterns

Mock CallData

import { CallData, Address, TokenBalance } from '@tevm/voltaire';
import { describe, it, expect } from 'vitest';

describe('CallData routing', () => {
  it('routes transfer correctly', () => {
    const calldata = abi.transfer.encode(
      Address("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
      TokenBalance.fromUnits("1", 18)
    );

    const route = getRoute(calldata);
    expect(route).toBe('transfer');
  });

  it('rejects unknown selector', () => {
    // Create calldata with unknown selector
    const calldata = CallData.fromHex("0x12345678");

    expect(() => getRoute(calldata)).toThrow('Unknown function');
  });
});

Selector Constants

// Define as constants for testing and routing
export const SELECTORS = {
  TRANSFER: [0xa9, 0x05, 0x9c, 0xbb] as [number, number, number, number],
  APPROVE: [0x09, 0x5e, 0xa7, 0xb3] as [number, number, number, number],
  TRANSFER_FROM: [0x23, 0xb8, 0x72, 0xdd] as [number, number, number, number],
} as const;

// Usage in tests
import { SELECTORS } from './constants.js';

it('computes transfer selector correctly', () => {
  const calldata = abi.transfer.encode(addr, amount);
  const selector = CallData.getSelector(calldata);

  expect(selector).toEqual(SELECTORS.TRANSFER);
});

Memory Management (Zig)

Proper Cleanup

pub fn processCallData(
    allocator: std.mem.Allocator,
    calldata: CallData,
) !void {
    // Decode owns memory
    var decoded = try calldata.decode(allocator);
    defer decoded.deinit(); // Always cleanup

    // Process parameters
    for (decoded.parameters) |param| {
        switch (param) {
            .address => |addr| try handleAddress(addr),
            .uint256 => |val| try handleUint256(val),
            else => return error.UnsupportedType,
        }
    }
}

Arena Allocator Pattern

pub fn batchProcessCalls(
    allocator: std.mem.Allocator,
    calls: []const CallData,
) !void {
    // Use arena for batch processing
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit(); // Free all at once

    const arena_allocator = arena.allocator();

    for (calls) |call| {
        var decoded = try call.decode(arena_allocator);
        // No need for individual deinit
        try processDecoded(decoded);
    }
    // Arena cleanup frees everything
}

See Also