Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
Contract Interaction
This guide covers reading contract state, writing transactions, and manual ABI encoding/decoding using Voltaire primitives.
Voltaire provides a reference implementation for Contract abstraction, not a library export. Copy the implementation from Contract Pattern into your project and customize as needed. See Primitives Philosophy for why.
Prerequisites
- A provider (EIP-1193 compatible)
- Contract address and ABI
- Contract implementation copied from Contract Pattern or
examples/contract/
Setup
// Contract.js copied from examples/contract/ or docs
import { Contract } from './Contract.js';
// Define ABI with 'as const' for type inference
const erc20Abi = [
{
type: 'function',
name: 'name',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'string', name: '' }]
},
{
type: 'function',
name: 'symbol',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'string', name: '' }]
},
{
type: 'function',
name: 'decimals',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'uint8', name: '' }]
},
{
type: 'function',
name: 'balanceOf',
stateMutability: 'view',
inputs: [{ type: 'address', name: 'account' }],
outputs: [{ type: 'uint256', name: '' }]
},
{
type: 'function',
name: 'transfer',
stateMutability: 'nonpayable',
inputs: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'amount' }
],
outputs: [{ type: 'bool', name: '' }]
},
{
type: 'function',
name: 'approve',
stateMutability: 'nonpayable',
inputs: [
{ type: 'address', name: 'spender' },
{ type: 'uint256', name: 'amount' }
],
outputs: [{ type: 'bool', name: '' }]
}
] as const;
// Create contract instance
const usdc = Contract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
provider
});
Always use as const when defining ABIs. This enables TypeScript to infer exact function names and parameter types.
Reading Contract State
The read interface executes view/pure functions via eth_call. No gas required.
Single Read
const balance = await usdc.read.balanceOf('0x742d35Cc6634C0532925a3b844Bc454e4438f44e');
console.log(balance); // 1000000n (bigint)
Multiple Reads in Parallel
const [name, symbol, decimals] = await Promise.all([
usdc.read.name(),
usdc.read.symbol(),
usdc.read.decimals()
]);
console.log(`${name} (${symbol}) - ${decimals} decimals`);
// "USD Coin (USDC) - 6 decimals"
How Read Works
Under the hood, read methods:
- ABI-encode the function call with arguments
- Send
eth_call to the provider
- ABI-decode the return data
// This:
const balance = await usdc.read.balanceOf('0x742d35...');
// Is equivalent to:
const calldata = usdc.abi.encode('balanceOf', ['0x742d35...']);
const result = await provider.request({
method: 'eth_call',
params: [{ to: usdc.address, data: calldata }, 'latest']
});
const decoded = usdc.abi.decode('balanceOf', result);
Writing to Contracts
The write interface sends transactions via eth_sendTransaction. Requires gas and modifies state.
Send Transaction
const txHash = await usdc.write.transfer(
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
1000000n // 1 USDC (6 decimals)
);
console.log(`Transaction: ${txHash}`);
Approve Spender
const txHash = await usdc.write.approve(
'0xRouterAddress...',
1000000000n // Approve 1000 USDC
);
Write methods return immediately after transaction submission. The transaction may still be pending or could revert during execution.
Estimate Gas First
Use estimateGas to simulate before sending:
try {
const gas = await usdc.estimateGas.transfer('0x742d35...', 1000000n);
console.log(`Estimated gas: ${gas}`);
// Safe to send
const txHash = await usdc.write.transfer('0x742d35...', 1000000n);
} catch (error) {
console.error('Transaction would fail:', error);
}
Manual ABI Encoding
For advanced use cases, access the abi instance directly.
Encode Function Call
// Encode transfer(address,uint256) calldata
const calldata = usdc.abi.encode('transfer', [
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
1000000n
]);
// calldata is Uint8Array: 0xa9059cbb + encoded params
console.log(calldata);
Decode Return Values
// Simulate a call and decode the result
const result = await provider.request({
method: 'eth_call',
params: [{
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
data: usdc.abi.encode('balanceOf', ['0x742d35...'])
}, 'latest']
});
// Decode the return value
const decoded = usdc.abi.decode('balanceOf', result);
console.log(decoded); // [1000000n]
Build Custom Transaction
import { Hex } from '@tevm/voltaire/Hex';
const calldata = usdc.abi.encode('transfer', ['0x...', 1000000n]);
const tx = {
to: Hex.fromBytes(usdc.address),
data: Hex.fromBytes(calldata),
gas: '0x5208'
};
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [tx]
});
Return Value Handling
Single Output
Functions with one output return the value directly:
const balance: bigint = await usdc.read.balanceOf('0x...');
Multiple Outputs
Functions with multiple outputs return an array:
const pairAbi = [{
type: 'function',
name: 'getReserves',
stateMutability: 'view',
inputs: [],
outputs: [
{ type: 'uint112', name: 'reserve0' },
{ type: 'uint112', name: 'reserve1' },
{ type: 'uint32', name: 'blockTimestampLast' }
]
}] as const;
const pair = Contract({ address: pairAddress, abi: pairAbi, provider });
const [reserve0, reserve1, timestamp] = await pair.read.getReserves();
Error Handling
// Errors are defined in your local Contract implementation
import { ContractReadError, ContractWriteError } from './errors.js';
try {
const balance = await usdc.read.balanceOf('0x...');
} catch (error) {
if (error instanceof ContractReadError) {
console.error('Read failed:', error.message);
}
}
try {
const txHash = await usdc.write.transfer('0x...', 1000n);
} catch (error) {
if (error instanceof ContractWriteError) {
console.error('Write failed:', error.message);
}
}
Complete Example
// Contract.js copied from examples/contract/ or docs
import { Contract } from './Contract.js';
import type { TypedProvider } from '@tevm/voltaire/provider';
const erc20Abi = [
{
type: 'function',
name: 'balanceOf',
stateMutability: 'view',
inputs: [{ type: 'address', name: 'account' }],
outputs: [{ type: 'uint256', name: '' }]
},
{
type: 'function',
name: 'transfer',
stateMutability: 'nonpayable',
inputs: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'amount' }
],
outputs: [{ type: 'bool', name: '' }]
}
] as const;
async function transferTokens(
provider: TypedProvider,
tokenAddress: string,
recipient: string,
amount: bigint
) {
const token = Contract({
address: tokenAddress,
abi: erc20Abi,
provider
});
// Check balance first
const balance = await token.read.balanceOf(recipient);
console.log(`Current balance: ${balance}`);
// Estimate gas
const gas = await token.estimateGas.transfer(recipient, amount);
console.log(`Estimated gas: ${gas}`);
// Send transaction
const txHash = await token.write.transfer(recipient, amount);
console.log(`Transaction sent: ${txHash}`);
return txHash;
}