Skip to main content

Install

pnpm add voltaire-effect @tevm/voltaire effect
voltaire-effect requires Effect 3.x and Voltaire 0.x. TypeScript 5.4+ recommended.
Runtime support:
  • Browser/WASM: BrowserTransport (EIP-1193) or HTTP; native HD wallet is unavailable.
  • Node/Bun: Full transport support + HD wallet (native FFI).

First Program

Fetch the latest block number:
import { Effect, Layer } from 'effect'
import { getBlockNumber, Provider, HttpTransport } from 'voltaire-effect'

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

const program = Effect.gen(function* () {
  return yield* getBlockNumber()
})

const blockNumber = await Effect.runPromise(program.pipe(Effect.provide(ProviderLayer)))
console.log(`Block: ${blockNumber}`)

Schema Validation

Validate and parse input:
import * as Address from 'voltaire-effect/primitives/Address'
import * as S from 'effect/Schema'

// Decode from hex string
const addr = S.decodeSync(Address.Hex)('0x742d35Cc6634C0532925a3b844Bc9e7595f251e3')

// Returns actual Voltaire branded type - use directly
Address.equals(addr, addr) // true
Address.isZero(addr) // false
Address.toHex(addr)  // "0x742d35cc..."
Most APIs accept AddressInput (either a branded AddressType or a 0x hex string). Decode user input at boundaries, then pass branded types internally. See Branded Types.

Effect-Based Validation

Handle parse errors gracefully:
import * as Address from 'voltaire-effect/primitives/Address'
import * as S from 'effect/Schema'
import * as Effect from 'effect/Effect'

const parseAddress = (input: string) =>
  S.decode(Address.Hex)(input).pipe(
    Effect.catchTag('ParseError', () => 
      Effect.succeed(Address.zero())  // Fallback to zero address
    )
  )

Checksummed Addresses

EIP-55 checksumming requires the Keccak hash service:
import * as Address from 'voltaire-effect/primitives/Address'
import * as S from 'effect/Schema'
import * as Effect from 'effect/Effect'
import { KeccakLive } from 'voltaire-effect/crypto/Keccak256'

const addr = S.decodeSync(Address.Hex)('0x742d35Cc6634C0532925a3b844Bc9e7595f251e3')

// Encode to checksummed (requires KeccakService)
const checksummed = await Effect.runPromise(
  S.encode(Address.Checksummed)(addr).pipe(Effect.provide(KeccakLive))
)
// "0x742d35Cc6634C0532925a3b844Bc9e7595f251e3"

Read Contract

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

// Compose provider layer once
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

const erc20 = [
  { type: 'function', name: 'balanceOf', inputs: [{ name: 'account', type: 'address' }], 
    outputs: [{ type: 'uint256' }], stateMutability: 'view' }
] as const

const getBalance = Effect.gen(function* () {
  const usdc = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20)
  return yield* usdc.read.balanceOf('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
})

const balance = await Effect.runPromise(getBalance.pipe(Effect.provide(ProviderLayer)))

Timeout & Retry

Per-request overrides and transport configuration use Effect-native Duration and Schedule:
import { Effect, Schedule } from 'effect'
import { getBalance, Provider, HttpTransport, withTimeout, withRetrySchedule } from 'voltaire-effect'

// Per-request timeout
getBalance(addr).pipe(withTimeout("5 seconds"))

// Per-request retry schedule
getBalance(addr).pipe(
  withRetrySchedule(Schedule.exponential("500 millis").pipe(
    Schedule.jittered,
    Schedule.compose(Schedule.recurs(3))
  ))
)

// Transport-level defaults
HttpTransport({
  url: 'https://eth.llamarpc.com',
  timeout: '30 seconds',
  retrySchedule: Schedule.exponential('500 millis').pipe(
    Schedule.jittered,
    Schedule.compose(Schedule.recurs(5))
  )
})

Next Steps