Skip to main content

Mock Transport

import { Effect, Schedule, Layer } from 'effect'
import { getBlockNumber, Provider, TestTransport, withTimeout, withoutCache } from 'voltaire-effect'
import { describe, it, expect } from 'vitest'

describe('Provider', () => {
  it('returns mocked block number', async () => {
    // Compose layers first
    const TestLayer = Provider.pipe(
      Layer.provide(TestTransport({ eth_blockNumber: () => '0x1234' }))
    )

    const program = Effect.gen(function* () {
      return yield* getBlockNumber()
    }).pipe(
      withTimeout('5 seconds'),
      Effect.provide(TestLayer)
    )

    expect(await Effect.runPromise(program)).toBe(4660n)
  })
})

Mock Multiple Methods

import { Effect, Layer } from 'effect'
import { getBlockNumber, getBalance, getBlock, Provider, TestTransport, withoutCache } from 'voltaire-effect'

const mockTransport = TestTransport({
  eth_blockNumber: () => '0x1234',
  eth_getBalance: (params) => params[0] === '0xRich...' ? '0xde0b6b3a7640000' : '0x0',
  eth_getBlock: () => ({ number: '0x1234', hash: '0x...', transactions: [] }),
  eth_call: (params) => params[0]?.data?.startsWith('0x70a08231')
    ? '0x0000000000000000000000000000000000000000000000000000000005f5e100'
    : '0x'
})

// Compose layers first, then provide once
const TestLayer = Layer.merge(Provider, mockTransport)

const program = Effect.gen(function* () {
  return yield* Effect.all({
    blockNumber: getBlockNumber(),
    balance: getBalance('0xRich...', 'latest'),
    block: getBlock({ blockTag: 'latest' })
  })
}).pipe(withoutCache, Effect.provide(TestLayer))

Test Contract Reads

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

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

it('reads balance', async () => {
  // Compose layers first
  const TestLayer = Provider.pipe(
    Layer.provide(TestTransport({
      eth_call: () => '0x0000000000000000000000000000000000000000000000056bc75e2d63100000'
    }))
  )

  const program = Effect.gen(function* () {
    const token = yield* Contract('0xToken...', erc20Abi)
    return yield* token.read.balanceOf('0xUser...')
  }).pipe(Effect.provide(TestLayer))

  expect(await Effect.runPromise(program)).toBe(100000000000000000000n)
})

Test Error Handling

import { Effect, Exit, Layer } from 'effect'
import { getBalance, Provider, TestTransport } from 'voltaire-effect'

it('handles RPC errors', async () => {
  // Compose layers first
  const TestLayer = Provider.pipe(
    Layer.provide(TestTransport({ eth_getBalance: () => { throw new Error('Rate limited') } }))
  )

  const program = Effect.gen(function* () {
    return yield* getBalance('0x...', 'latest')
  }).pipe(Effect.provide(TestLayer))

  const exit = await Effect.runPromiseExit(program)
  expect(Exit.isFailure(exit)).toBe(true)
})

it('recovers from errors', async () => {
  // Compose layers first
  const TestLayer = Provider.pipe(
    Layer.provide(TestTransport({ eth_getBalance: () => { throw new Error('Failed') } }))
  )

  const program = Effect.gen(function* () {
    return yield* getBalance('0x...', 'latest')
  }).pipe(
    Effect.catchTag('TransportError', () => Effect.succeed(0n)),
    Effect.provide(TestLayer)
  )

  expect(await Effect.runPromise(program)).toBe(0n)
})

Test Crypto

Each crypto module exports test layers for deterministic mocking:
import { hash, KeccakTest } from 'voltaire-effect/crypto/Keccak256'

it('hashes data', async () => {
  const result = await Effect.runPromise(
    hash(new Uint8Array([1, 2, 3])).pipe(Effect.provide(KeccakTest))
  )
  expect(result).toBeInstanceOf(Uint8Array)
  expect(result.length).toBe(32)
})

All Crypto Services

Use CryptoLive and CryptoTest convenience layers to provide all crypto services at once:
import { CryptoLive, CryptoTest } from 'voltaire-effect'
import { KeccakService } from 'voltaire-effect/crypto/Keccak256'
import { Secp256k1Service } from 'voltaire-effect/crypto/Secp256k1'

// Production - all crypto services in one layer
const program = Effect.gen(function* () {
  const keccak = yield* KeccakService
  const secp = yield* Secp256k1Service
  // All crypto services available
}).pipe(Effect.provide(CryptoLive))

// Testing - deterministic mocks for all crypto
const testProgram = program.pipe(Effect.provide(CryptoTest))

Integration Test (Anvil)

import { Effect, Layer, Schedule } from 'effect'
import {
  SignerService, Signer, LocalAccount, Provider, HttpTransport,
  getBalance, waitForTransactionReceipt
} from 'voltaire-effect'
import { Hex } from '@tevm/voltaire'

const TestLayer = Layer.mergeAll(
  Provider,
  Signer.Live,
  LocalAccount(Hex.fromHex('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80')),
  HttpTransport({
    url: 'http://localhost:8545',
    timeout: '10 seconds',
    retrySchedule: Schedule.recurs(1)
  })
)

it('sends transaction', async () => {
  const program = Effect.gen(function* () {
    const signer = yield* SignerService

    const before = yield* getBalance('0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 'latest')
    const txHash = yield* signer.sendTransaction({ to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: 1000000000000000000n })
    yield* waitForTransactionReceipt(txHash, { confirmations: 1, timeout: '30 seconds' })
    const after = yield* getBalance('0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 'latest')

    return after - before
  }).pipe(Effect.provide(TestLayer))

  expect(await Effect.runPromise(program)).toBe(1000000000000000000n)
})

Snapshot Testing

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'

it('checksums correctly', async () => {
  const addr = S.decodeSync(Address.Hex)('0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed')
  const program = S.encode(Address.Checksummed)(addr).pipe(Effect.provide(KeccakLive))
  
  expect(await Effect.runPromise(program)).toMatchInlineSnapshot('"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"')
})

Property-Based Testing

import * as Address from 'voltaire-effect/primitives/Address'
import * as S from 'effect/Schema'
import * as fc from 'fast-check'

it('round-trips through hex', async () => {
  await fc.assert(fc.asyncProperty(
    fc.hexaString({ minLength: 40, maxLength: 40 }),
    async (hex) => {
      const addr = S.decodeSync(Address.Hex)(`0x${hex}`)
      const encoded = S.encodeSync(Address.Hex)(addr)
      const roundTripped = S.decodeSync(Address.Hex)(encoded)
      expect(Address.equals(addr, roundTripped)).toBe(true)
    }
  ))
})

See Also