Skip to main content
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