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