Skip to main content
This page shows common contract patterns. See Contract Factory for complete API and ERC Standards for token utilities.

Read Contract

import { Effect, Duration, Schedule, Layer } from 'effect'
import { Contract, Provider, HttpTransport, withTimeout, withRetrySchedule } from 'voltaire-effect'

const erc20Abi = [
  { type: 'function', name: 'balanceOf', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
  { type: 'function', name: 'symbol', inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
  { type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }
] as const

// Compose layers first, then provide once
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://mainnet.infura.io/v3/YOUR_KEY'))
)

const program = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)
  return yield* Effect.all({
    symbol: token.read.symbol(),
    decimals: token.read.decimals(),
    balance: token.read.balanceOf('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
  })
}).pipe(Effect.provide(ProviderLayer))

Write Contract

import { Layer } from 'effect'
import { Signer, LocalAccount, Provider, HttpTransport } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'
import { Hex } from '@tevm/voltaire'

const privateKey = Hex.fromHex('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80')

// Compose all layers first
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const SignerLayer = Layer.mergeAll(
  Signer.fromPrivateKey(privateKey, Provider),
  CryptoLayer
).pipe(Layer.provide(TransportLayer))

const program = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)
  return yield* token.write.transfer('0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 100_000_000n)
}).pipe(Effect.provide(SignerLayer))

Simulate (Dry Run)

Simulate a write before sending to catch reverts:
// Compose layers first
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

const program = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)
  return yield* token.simulate.transfer('0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 100_000_000n)
}).pipe(Effect.provide(ProviderLayer))

Query Events

Query historical Transfer events from the blockchain:
const eventAbi = [
  { type: 'event', name: 'Transfer', inputs: [
    { name: 'from', type: 'address', indexed: true },
    { name: 'to', type: 'address', indexed: true },
    { name: 'value', type: 'uint256', indexed: false }
  ]}
] as const

// Compose layers first
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

const program = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', eventAbi)
  return yield* token.getEvents('Transfer', { fromBlock: 'latest', toBlock: 'latest' })
}).pipe(Effect.provide(ProviderLayer))

Complete ERC20 Workflow

A full workflow: read token info, transfer, wait for confirmation, verify balance change:
import { Effect, Layer } from 'effect'
import { Contract, waitForTransactionReceipt, Signer, Provider, HttpTransport } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'

// Compose all layers first
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const SignerLayer = Layer.mergeAll(
  Signer.fromPrivateKey(privateKey, Provider),
  CryptoLayer
).pipe(Layer.provide(TransportLayer))

const program = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)

  const { symbol, decimals } = yield* Effect.all({ symbol: token.read.symbol(), decimals: token.read.decimals() })
  const balanceBefore = yield* token.read.balanceOf('0x70997970C51812dc3A010C7d01b50e0d17dc79C8')
  const txHash = yield* token.write.transfer('0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 100_000_000n)
  yield* waitForTransactionReceipt(txHash, { confirmations: 1 })
  const balanceAfter = yield* token.read.balanceOf('0x70997970C51812dc3A010C7d01b50e0d17dc79C8')

  return { symbol, decimals, txHash, transferred: balanceAfter - balanceBefore }
}).pipe(Effect.provide(SignerLayer))

Uniswap V2 Swap

A real-world example using Uniswap V2 Router:
const routerAbi = [
  { type: 'function', name: 'swapExactTokensForTokens', inputs: [
    { name: 'amountIn', type: 'uint256' }, { name: 'amountOutMin', type: 'uint256' },
    { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }
  ], outputs: [{ type: 'uint256[]' }], stateMutability: 'nonpayable' },
  { type: 'function', name: 'getAmountsOut', inputs: [
    { name: 'amountIn', type: 'uint256' }, { name: 'path', type: 'address[]' }
  ], outputs: [{ type: 'uint256[]' }], stateMutability: 'view' }
] as const

const myAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'

// Compose all layers first
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const SignerLayer = Layer.mergeAll(
  Signer.fromPrivateKey(privateKey, Provider),
  CryptoLayer
).pipe(Layer.provide(TransportLayer))

const program = Effect.gen(function* () {
  const router = yield* Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', routerAbi)
  const path = ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2']

  const amounts = yield* router.read.getAmountsOut(1000_000_000n, path)
  const amountOutMin = (amounts[1] * 95n) / 100n  // 5% slippage
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200)  // 20 minutes

  return yield* router.write.swapExactTokensForTokens(1000_000_000n, amountOutMin, path, myAddress, deadline)
}).pipe(Effect.provide(SignerLayer))

See Also

Timeouts and Retries

Use Effect-idiomatic helpers for timeouts and retries:
// Compose layers first
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://mainnet.infura.io/v3/YOUR_KEY'))
)

const resilientRead = Effect.gen(function* () {
  const token = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)
  return yield* token.read.balanceOf('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
}).pipe(
  // Timeout with Duration string or Duration.seconds(5)
  withTimeout("5 seconds"),
  // Retry with Effect Schedule
  withRetrySchedule(
    Schedule.exponential("500 millis").pipe(
      Schedule.jittered,
      Schedule.compose(Schedule.recurs(3))
    )
  ),
  Effect.provide(ProviderLayer)
)