Skip to main content
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.

Source & Tests

Contract Pattern

A typed contract abstraction you copy into your codebase. Built on Voltaire primitives, customizable for your needs.
This is a Skill, not a library export. Copy the code below into your project and modify as needed. See Skills Philosophy for why Voltaire uses copyable implementations instead of rigid library abstractions.

Why Copy Instead of Import?

BenefitDescription
AI ContextLLMs see your full implementation, can modify and debug it
CustomizableAdd methods, change error handling, add caching
No Lock-inModify freely, no library updates to worry about
Right-sizedRemove what you don’t need
This implementation follows ethers.js patterns (contract.read.method(), contract.write.method()). LLMs have extensive training data on ethers and will write better code with familiar APIs.

Quick Start

import { Contract } from './Contract.js';  // Your local copy

const usdc = Contract({
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  abi: erc20Abi,
  provider
});

// Read
const balance = await usdc.read.balanceOf('0x...');

// Write
const txHash = await usdc.write.transfer('0x...', 1000n);

// Estimate gas
const gas = await usdc.estimateGas.transfer('0x...', 1000n);

// Stream events
const stream = usdc.events.Transfer({ from: '0x...' });
for await (const { log } of stream.backfill({ fromBlock: 18000000n, toBlock: 19000000n })) {
  console.log(log.args.value);
}

Implementation

Copy these files into your project:
/**
 * Contract Factory
 *
 * Creates typed contract instances for interacting with deployed smart contracts.
 */

import { Abi } from '@tevm/voltaire/Abi';
import { Address } from '@tevm/voltaire/Address';
import { Hex } from '@tevm/voltaire/Hex';
import * as TransactionHash from '@tevm/voltaire/TransactionHash';
// If you copied EventStream from this guide, import locally:
import { EventStream } from './EventStream.js';

class ContractFunctionNotFoundError extends Error {
  name = 'ContractFunctionNotFoundError';
  constructor(functionName) {
    super(`Function "${functionName}" not found in contract ABI`);
  }
}

class ContractEventNotFoundError extends Error {
  name = 'ContractEventNotFoundError';
  constructor(eventName) {
    super(`Event "${eventName}" not found in contract ABI`);
  }
}

class ContractReadError extends Error {
  name = 'ContractReadError';
  constructor(functionName, cause) {
    super(`Failed to read "${functionName}" from contract`);
    this.cause = cause;
  }
}

class ContractWriteError extends Error {
  name = 'ContractWriteError';
  constructor(functionName, cause) {
    super(`Failed to write "${functionName}" to contract`);
    this.cause = cause;
  }
}

export function Contract(options) {
  const { abi: abiItems, provider } = options;
  const address = Address.from(options.address);
  const abi = Abi(abiItems);
  const addressHex = Hex.fromBytes(address);

  // Read methods (view/pure) via eth_call
  const read = new Proxy({}, {
    get(_target, prop) {
      if (typeof prop !== 'string') return undefined;
      const functionName = prop;

      return async (...args) => {
        const fn = abi.getFunction(functionName);
        if (!fn || (fn.stateMutability !== 'view' && fn.stateMutability !== 'pure')) {
          throw new ContractFunctionNotFoundError(functionName);
        }

        try {
          const data = abi.encode(functionName, args);
          const result = await provider.request({
            method: 'eth_call',
            params: [{ to: addressHex, data: Hex.fromBytes(data) }, 'latest'],
          });
          const decoded = abi.decode(functionName, Hex.toBytes(result));
          return decoded.length === 1 ? decoded[0] : decoded;
        } catch (error) {
          if (error instanceof ContractFunctionNotFoundError) throw error;
          throw new ContractReadError(functionName, error);
        }
      };
    },
  });

  // Write methods (nonpayable/payable) via eth_sendTransaction
  const write = new Proxy({}, {
    get(_target, prop) {
      if (typeof prop !== 'string') return undefined;
      const functionName = prop;

      return async (...args) => {
        const fn = abi.getFunction(functionName);
        if (!fn || (fn.stateMutability !== 'nonpayable' && fn.stateMutability !== 'payable')) {
          throw new ContractFunctionNotFoundError(functionName);
        }

        try {
          const data = abi.encode(functionName, args);
          const txHash = await provider.request({
            method: 'eth_sendTransaction',
            params: [{ to: addressHex, data: Hex.fromBytes(data) }],
          });
          return TransactionHash.fromHex(txHash);
        } catch (error) {
          if (error instanceof ContractFunctionNotFoundError) throw error;
          throw new ContractWriteError(functionName, error);
        }
      };
    },
  });

  // Gas estimation via eth_estimateGas
  const estimateGas = new Proxy({}, {
    get(_target, prop) {
      if (typeof prop !== 'string') return undefined;
      const functionName = prop;

      return async (...args) => {
        const fn = abi.getFunction(functionName);
        if (!fn || (fn.stateMutability !== 'nonpayable' && fn.stateMutability !== 'payable')) {
          throw new ContractFunctionNotFoundError(functionName);
        }

        const data = abi.encode(functionName, args);
        const gasHex = await provider.request({
          method: 'eth_estimateGas',
          params: [{ to: addressHex, data: Hex.fromBytes(data) }],
        });
        return BigInt(gasHex);
      };
    },
  });

  // Events - returns EventStream instances
  const events = new Proxy({}, {
    get(_target, prop) {
      if (typeof prop !== 'string') return undefined;
      const eventName = prop;

      return (filter) => {
        const event = abi.getEvent(eventName);
        if (!event) throw new ContractEventNotFoundError(eventName);
        return EventStream({ provider, address, event, filter });
      };
    },
  });

  return { address, abi, read, write, estimateGas, events };
}

API Reference

Contract Factory

function Contract<TAbi extends readonly Item[]>(options: {
  address: AddressType | `0x${string}`;
  abi: TAbi;
  provider: TypedProvider;
}): ContractInstance<TAbi>;

ContractInstance

PropertyTypeDescription
addressAddressTypeContract address
abiAbi<TAbi>ABI instance with encode/decode methods
readContractReadMethods<TAbi>View/pure function calls
writeContractWriteMethods<TAbi>State-changing transactions
estimateGasContractEstimateGasMethods<TAbi>Gas estimation
eventsContractEventFilters<TAbi>Event streaming

Customization Ideas

Add Caching

const read = new Proxy({}, {
  get(_target, prop) {
    // ... existing code ...
    return async (...args) => {
      const cacheKey = `${functionName}:${JSON.stringify(args)}`;
      if (cache.has(cacheKey)) return cache.get(cacheKey);

      const result = /* ... call eth_call ... */;
      cache.set(cacheKey, result);
      return result;
    };
  },
});

Add Retry Logic

const result = await retry(
  () => provider.request({ method: 'eth_call', params }),
  { retries: 3, delay: 1000 }
);

Add Custom Methods

return {
  address, abi, read, write, estimateGas, events,

  // Custom: Get all token info at once
  async getTokenInfo() {
    const [name, symbol, decimals] = await Promise.all([
      this.read.name(),
      this.read.symbol(),
      this.read.decimals(),
    ]);
    return { name, symbol, decimals };
  },
};