Skip to main content
Contract() is a factory function that returns a type-safe contract instance. It is not a Context.Tag service—instead it depends on ProviderService (and optionally SignerService for writes).
For applications with multiple contracts, use ContractRegistryService to define all your contracts once and access them as a named map.

Quick Start

import { Contract, Provider, HttpTransport } from 'voltaire-effect'
import { Effect, Layer } from 'effect'

const erc20Abi = [
  { type: 'function', name: 'balanceOf', stateMutability: 'view',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: 'balance', type: 'uint256' }] },
  { type: 'function', name: 'transfer', stateMutability: 'nonpayable',
    inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
    outputs: [{ name: 'success', type: 'bool' }] },
] as const

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

const program = Effect.gen(function* () {
  // Contract() is a factory that yields a contract instance
  const dai = yield* Contract(daiAddress, erc20Abi)
  const balance = yield* dai.read.balanceOf(userAddress)
  return balance
}).pipe(Effect.provide(ProviderLayer))

Read

View/pure functions via eth_call. No wallet required.
const balance = yield* contract.read.balanceOf(userAddress)

Write

State-changing functions. Requires SignerService.
const txHash = yield* contract.write.transfer(recipientAddress, 1000n)

Simulate

Test writes without sending.
const success = yield* contract.simulate.transfer(recipientAddress, 1000n)

Events

const transfers = yield* contract.getEvents('Transfer', {
  fromBlock: 18000000n,
  toBlock: 'latest',
  args: { from: userAddress }
})

Type Safety

Types inferred from ABI:
contract.read.balanceOf(address)     // ✅ takes address
contract.read.balanceOf(123n)        // ❌ type error
contract.write.transfer(to, amount)  // ✅ write method
contract.write.balanceOf(address)    // ❌ balanceOf is view

Request Configuration

Use Effect-native helpers for per-request timeout and retry:
import { withTimeout, withRetrySchedule, withoutCache, withTracing } from 'voltaire-effect'
import { Schedule } from 'effect'

// Read with custom timeout
const balance = yield* token.read.balanceOf(user).pipe(
  withTimeout("5 seconds")
)

// Write with retry on transient errors
const txHash = yield* token.write.transfer(recipient, 100n).pipe(
  withRetrySchedule(
    Schedule.exponential("500 millis").pipe(
      Schedule.jittered,
      Schedule.compose(Schedule.recurs(3))
    )
  )
)

// Bypass cache for fresh data
const freshBalance = yield* token.read.balanceOf(user).pipe(withoutCache)

// Enable tracing for debugging
const debugBalance = yield* token.read.balanceOf(user).pipe(withTracing())

Error Handling

import { ContractCallError, ContractWriteError } from 'voltaire-effect'

program.pipe(
  Effect.catchTag('ContractCallError', (e) => Effect.succeed({ error: e.message })),
  Effect.catchTag('ContractWriteError', (e) => Effect.succeed({ error: e.message }))
)

Full Example

Complete workflow with writes:
import { Effect } from 'effect'
import { Contract, Signer, LocalAccount, Provider, HttpTransport } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'
import { Hex } from '@tevm/voltaire'

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

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

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

  // Read before
  const balanceBefore = yield* token.read.balanceOf(recipient)

  // Simulate
  const success = yield* token.simulate.transfer(recipient, 100n)
  if (!success) return yield* Effect.fail(new Error('Transfer would fail'))

  // Write
  const txHash = yield* token.write.transfer(recipient, 100n)

  return { txHash, balanceBefore }
}).pipe(Effect.provide(SignerLayer))

See Also