Skip to main content

Quick Start

import { Effect } from 'effect'
import { ChainService, mainnet } from 'voltaire-effect'

const program = Effect.gen(function* () {
  const chain = yield* ChainService
  console.log(`Connected to ${chain.name} (${chain.id})`)
  return chain
}).pipe(Effect.provide(mainnet))

Predefined Chains

voltaire-effect provides Layer configurations for common networks:
import { mainnet, sepolia, optimism, arbitrum, base, polygon } from 'voltaire-effect'

// Ethereum Mainnet (id: 1)
Effect.provide(mainnet)

// Sepolia Testnet (id: 11155111)
Effect.provide(sepolia)

// Optimism (id: 10)
Effect.provide(optimism)

// Arbitrum One (id: 42161)
Effect.provide(arbitrum)

// Base (id: 8453)
Effect.provide(base)

// Polygon (id: 137)
Effect.provide(polygon)

ChainConfig Interface

interface ChainConfig {
  readonly id: number                    // Chain ID
  readonly name: string                  // Human-readable name
  readonly nativeCurrency: {
    readonly name: string                // e.g., "Ether"
    readonly symbol: string              // e.g., "ETH"
    readonly decimals: number            // Usually 18
  }
  readonly blockTime: number             // Average block time in ms
  readonly testnet?: boolean
}

interface BlockExplorerConfig {
  readonly default?: {
    readonly name: string
    readonly url: string
    readonly apiUrl?: string
  }
}

interface ContractsConfig {
  readonly multicall3?: ChainContract
  readonly ensRegistry?: ChainContract
  readonly ensUniversalResolver?: ChainContract
}

interface ChainContract {
  readonly address: `0x${string}`
  readonly blockCreated?: number
}
Block explorer metadata and contract deployments are provided by BlockExplorerService and ContractsService respectively.

RPC URLs

RPC endpoints are exported as a separate map keyed by chain ID:
import { rpcUrlsByChainId } from 'voltaire-effect'

const mainnetRpcUrls = rpcUrlsByChainId[1]
// mainnetRpcUrls.default.http[0] -> "https://eth.merkle.io"

Custom Chain Creation

Create custom chain configurations using Layer.mergeAll:
import { Layer } from 'effect'
import {
  BlockExplorerService,
  ChainConfig,
  ChainService,
  ContractsService
} from 'voltaire-effect'

const myChain = Layer.mergeAll(
  Layer.succeed(ChainService, {
    id: 12345,
    name: 'My Chain',
    nativeCurrency: { name: 'Token', symbol: 'TKN', decimals: 18 },
    blockTime: 2000
  } satisfies ChainConfig),
  Layer.succeed(BlockExplorerService, {
    default: { name: 'My Explorer', url: 'https://explorer.mychain.io' }
  }),
  Layer.succeed(ContractsService, {
    multicall3: {
      address: '0xcA11bde05977b3631167028862bE2a173976CA11',
      blockCreated: 1
    }
  })
)
Only provide the services you need; BlockExplorerService and ContractsService are optional for custom chains.

Usage with Other Services

Chain configuration is used by other services for chain-specific behavior:
import { Effect, Layer } from 'effect'
import {
  ChainService,
  ContractsService,
  getBlockNumber,
  Provider,
  HttpTransport,
  mainnet
} from 'voltaire-effect'

const program = Effect.gen(function* () {
  const chain = yield* ChainService
  const contracts = yield* ContractsService

  // Use chain-specific data
  const multicall = contracts.multicall3?.address
  const blockNumber = yield* getBlockNumber()

  return { chain: chain.name, multicall, blockNumber }
}).pipe(
  Effect.provide(Layer.merge(
    Provider.pipe(Layer.provide(HttpTransport('https://eth.llamarpc.com'))),
    mainnet
  ))
)

Service Interface

// ChainService is a Context.Tag that provides ChainConfig directly
class ChainService extends Context.Tag("ChainService")<
  ChainService,
  ChainConfig
>() {}

// BlockExplorerService provides block explorer metadata
class BlockExplorerService extends Context.Tag("BlockExplorerService")<
  BlockExplorerService,
  BlockExplorerConfig
>() {}

// ContractsService provides well-known contract deployments
class ContractsService extends Context.Tag("ContractsService")<
  ContractsService,
  ContractsConfig
>() {}