Skip to main content

Simple Provider Setup

Use MainnetProvider for the simplest setup:
import { Effect } from 'effect'
import { getBlockNumber, getChainId, MainnetProvider } from 'voltaire-effect'

const program = Effect.gen(function* () {
  const blockNumber = yield* getBlockNumber()
  const chainId = yield* getChainId()
  return { blockNumber, chainId }
}).pipe(Effect.provide(MainnetProvider('https://eth.llamarpc.com')))

await Effect.runPromise(program)

Multi-Chain Applications

Switch between chains by swapping presets:
import { Effect } from 'effect'
import {
  getBlockNumber as getBlockNum,
  MainnetProvider,
  OptimismProvider,
  ArbitrumProvider,
  BaseProvider
} from 'voltaire-effect'

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

// Run on different chains
const [mainnet, optimism, arbitrum, base] = await Promise.all([
  Effect.runPromise(getBlockNumber.pipe(
    Effect.provide(MainnetProvider('https://eth.llamarpc.com'))
  )),
  Effect.runPromise(getBlockNumber.pipe(
    Effect.provide(OptimismProvider('https://mainnet.optimism.io'))
  )),
  Effect.runPromise(getBlockNumber.pipe(
    Effect.provide(ArbitrumProvider('https://arb1.arbitrum.io/rpc'))
  )),
  Effect.runPromise(getBlockNumber.pipe(
    Effect.provide(BaseProvider('https://mainnet.base.org'))
  ))
])

console.log({ mainnet, optimism, arbitrum, base })

Full Service Stack

Use MainnetFullProvider for applications needing all services:
import { Effect } from 'effect'
import {
  getBlockNumber,
  ChainService,
  FeeEstimatorService,
  NonceManagerService,
  CacheService,
  MainnetFullProvider
} from 'voltaire-effect'

const program = Effect.gen(function* () {
  const chain = yield* ChainService
  const feeEstimator = yield* FeeEstimatorService
  const nonceManager = yield* NonceManagerService
  const cache = yield* CacheService

  // All services available
  const blockNumber = yield* getBlockNumber()
  const fees = yield* feeEstimator.estimate()
  const nonce = yield* nonceManager.getNextNonce('0x742d35Cc6634C0532925a3b844Bc9e7595f251e3')

  return {
    chain: chain.name,
    chainId: chain.id,
    blockNumber,
    fees,
    nonce
  }
}).pipe(Effect.provide(MainnetFullProvider('https://eth.llamarpc.com')))

L2 Applications

Chain-specific presets include proper chain configuration:
import { Effect } from 'effect'
import {
  getBlockNumber,
  ChainService,
  ContractsService,
  OptimismProvider
} from 'voltaire-effect'

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

  // Chain config is pre-populated
  console.log(`Chain: ${chain.name}`)          // "Optimism"
  console.log(`Chain ID: ${chain.id}`)          // 10
  console.log(`Block time: ${chain.blockTime}`) // 2000ms
  console.log(`Multicall3: ${contracts.multicall3?.address}`)

  const block = yield* getBlockNumber()
  return block
}).pipe(Effect.provide(OptimismProvider('https://mainnet.optimism.io')))

Testing with Presets

Replace transport for testing:
import { Effect, Layer } from 'effect'
import {
  getBlockNumber,
  Provider,
  TransportService,
  mainnet,
  DefaultFormatter,
  DefaultTransactionSerializer,
  DefaultFeeEstimator,
  DefaultNonceManager,
  MemoryCache
} from 'voltaire-effect'

// Mock transport
const MockTransport = Layer.succeed(TransportService, {
  request: (method: string, _params?: unknown[]) => {
    const responses: Record<string, unknown> = {
      eth_blockNumber: '0x1234',
      eth_chainId: '0x1',
      eth_getBalance: '0xde0b6b3a7640000'
    }
    return Effect.succeed(responses[method])
  }
})

// Test provider with same structure as MainnetFullProvider
const TestProvider = Layer.mergeAll(
  Provider.pipe(Layer.provide(MockTransport)),
  DefaultFormatter,
  DefaultTransactionSerializer.Live,
  DefaultFeeEstimator.pipe(Layer.provide(Provider.pipe(Layer.provide(MockTransport)))),
  DefaultNonceManager.pipe(Layer.provide(Provider.pipe(Layer.provide(MockTransport)))),
  MemoryCache(),
  mainnet
)

// Test
const testProgram = Effect.gen(function* () {
  return yield* getBlockNumber()
}).pipe(Effect.provide(TestProvider))

await Effect.runPromise(testProgram) // 4660n

createProvider for Custom Networks

For networks without a preset:
import { Effect } from 'effect'
import { getChainId, createProvider } from 'voltaire-effect'

// Any EVM-compatible network
const fantomProvider = createProvider('https://rpc.ftm.tools')
const avalancheProvider = createProvider('https://api.avax.network/ext/bc/C/rpc')
const bscProvider = createProvider('https://bsc-dataseed.binance.org')

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

// Fantom
await Effect.runPromise(program.pipe(Effect.provide(fantomProvider))) // 250n

// Avalanche
await Effect.runPromise(program.pipe(Effect.provide(avalancheProvider))) // 43114n

// BSC
await Effect.runPromise(program.pipe(Effect.provide(bscProvider))) // 56n

When to Use Manual Composition

Presets are convenient, but manual composition gives more control:
import { Effect, Layer } from 'effect'
import { 
  Provider, 
  HttpTransport,
  mainnet,
  DefaultFormatter
} from 'voltaire-effect'

// Custom layer with only what you need
const MinimalProvider = Layer.mergeAll(
  Provider.pipe(Layer.provide(HttpTransport('https://eth.llamarpc.com'))),
  mainnet
)

// Or with custom implementations
import { MyCustomFeeEstimator } from './custom-fee-estimator'

const CustomStack = Layer.mergeAll(
  Provider.pipe(Layer.provide(HttpTransport('https://eth.llamarpc.com'))),
  DefaultFormatter,
  mainnet,
  MyCustomFeeEstimator  // Your implementation
)