Skip to main content

Query Historical Events

Use Contract.getEvents for historical event queries:
import { Effect, Layer } from 'effect'
import { Contract, Provider, HttpTransport } from 'voltaire-effect'

// USDC contract
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'

const transferAbi = [
  {
    type: 'event',
    name: 'Transfer',
    inputs: [
      { name: 'from', type: 'address', indexed: true },
      { name: 'to', type: 'address', indexed: true },
      { name: 'value', type: 'uint256', indexed: false }
    ]
  }
] as const

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

const program = Effect.gen(function* () {
  const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

  const events = yield* usdc.getEvents('Transfer', {
    fromBlock: 18000000n,
    toBlock: 18000100n
  })

  for (const event of events) {
    console.log(`Transfer: ${event.args.from}${event.args.to} | ${event.args.value}`)
  }

  return events
}).pipe(Effect.provide(ProviderLayer))

await Effect.runPromise(program)

Filter by Indexed Parameters

Filter events efficiently at the RPC level using indexed parameters:
const VITALIK = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'

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

const program = Effect.gen(function* () {
  const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

  // Get transfers FROM Vitalik
  const fromVitalik = yield* usdc.getEvents('Transfer', {
    fromBlock: 18000000n,
    toBlock: 18100000n,
    args: { from: VITALIK }
  })

  // Get transfers TO Vitalik
  const toVitalik = yield* usdc.getEvents('Transfer', {
    fromBlock: 18000000n,
    toBlock: 18100000n,
    args: { to: VITALIK }
  })

  return { fromVitalik, toVitalik }
}).pipe(Effect.provide(ProviderLayer))

Large Block Range Backfill

For large block ranges, chunk requests to avoid RPC limits:
import { Effect, Chunk, Stream } from 'effect'

const backfillEvents = (fromBlock: bigint, toBlock: bigint, chunkSize = 2000n) =>
  Effect.gen(function* () {
    const usdc = yield* Contract(USDC_ADDRESS, transferAbi)
    const allEvents: typeof usdc.getEvents extends (...args: any) => Effect.Effect<infer R, any, any> ? R : never = []
    
    let currentBlock = fromBlock
    let eventCount = 0
    
    while (currentBlock <= toBlock) {
      const endBlock = currentBlock + chunkSize > toBlock ? toBlock : currentBlock + chunkSize
      
      const events = yield* usdc.getEvents('Transfer', {
        fromBlock: currentBlock,
        toBlock: endBlock
      })
      
      allEvents.push(...events)
      eventCount += events.length
      
      const progress = Number(currentBlock - fromBlock) / Number(toBlock - fromBlock) * 100
      yield* Effect.log(`Progress: ${progress.toFixed(1)}% | Block ${currentBlock} | Events: ${eventCount}`)
      
      currentBlock = endBlock + 1n
    }
    
    return allEvents
  })

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

// Backfill 100k blocks
await Effect.runPromise(backfillEvents(18000000n, 18100000n).pipe(Effect.provide(ProviderLayer)))

Live Event Watching with BlockStream

Combine BlockStream with event queries for live watching:
import { Effect, Stream, Layer } from 'effect'
import { makeBlockStream, Contract, Provider, HttpTransport } from 'voltaire-effect/services'

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

const watchLive = Effect.gen(function* () {
  const blockStream = yield* makeBlockStream()
  const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

  let lastProcessedBlock = 0n

  yield* Stream.runForEach(
    blockStream.watch({ include: 'header' }),
    (event) => Effect.gen(function* () {
      if (event.type === 'reorg') {
        yield* Effect.log(`Reorg detected: ${event.removed.length} blocks removed`)
        return
      }

      for (const block of event.blocks) {
        const blockNumber = BigInt(block.header.number)

        if (blockNumber <= lastProcessedBlock) continue

        const events = yield* usdc.getEvents('Transfer', {
          fromBlock: blockNumber,
          toBlock: blockNumber
        })

        for (const ev of events) {
          yield* Effect.log(`LIVE: ${ev.args.from}${ev.args.to} | ${ev.args.value}`)
        }

        lastProcessedBlock = blockNumber
      }
    })
  )
}).pipe(Effect.provide(ProviderLayer))

Historical to Live Transition

Start from historical events and seamlessly transition to live:
import { Effect, Stream, Layer } from 'effect'
import { Contract, Provider, HttpTransport, makeBlockStream, getBlockNumber } from 'voltaire-effect'

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

const historicalToLive = (startBlock: bigint) =>
  Effect.gen(function* () {
    const blockStream = yield* makeBlockStream()
    const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

    // Phase 1: Backfill historical events
    yield* Effect.log('Starting historical backfill...')
    const currentBlock = yield* getBlockNumber()

    let block = startBlock
    while (block < currentBlock) {
      const endBlock = block + 2000n > currentBlock ? currentBlock : block + 2000n

      const events = yield* usdc.getEvents('Transfer', {
        fromBlock: block,
        toBlock: endBlock
      })

      for (const ev of events) {
        yield* Effect.log(`HISTORICAL: ${ev.args.from}${ev.args.to}`)
      }

      block = endBlock + 1n
    }

    // Phase 2: Switch to live watching
    yield* Effect.log('Switching to live mode...')

    yield* Stream.runForEach(
      blockStream.watch({ include: 'header' }),
      (event) => Effect.gen(function* () {
        if (event.type === 'blocks') {
          for (const blk of event.blocks) {
            const events = yield* usdc.getEvents('Transfer', {
              fromBlock: BigInt(blk.header.number),
              toBlock: BigInt(blk.header.number)
            })

            for (const ev of events) {
              yield* Effect.log(`LIVE: ${ev.args.from}${ev.args.to}`)
            }
          }
        }
      })
    )
  }).pipe(Effect.provide(ProviderLayer))

Filter Large Transfers

Process events with filtering and transformation:
// Compose layers first
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

const whaleTransfers = Effect.gen(function* () {
  const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

  const events = yield* usdc.getEvents('Transfer', {
    fromBlock: 18000000n,
    toBlock: 18001000n
  })

  // Filter large transfers (> 100k USDC, 6 decimals)
  const whales = events.filter(e => e.args.value > 100_000_000000n)

  yield* Effect.log(`Found ${whales.length} whale transfers out of ${events.length} total`)

  return whales.map(e => ({
    from: e.args.from,
    to: e.args.to,
    value: Number(e.args.value) / 1e6,
    block: e.blockNumber,
    txHash: e.transactionHash
  }))
}).pipe(Effect.provide(ProviderLayer))

Multiple Event Types

Watch multiple event types from the same contract:
const multiEventAbi = [
  { 
    type: 'event', 
    name: 'Transfer', 
    inputs: [
      { name: 'from', type: 'address', indexed: true },
      { name: 'to', type: 'address', indexed: true },
      { name: 'value', type: 'uint256', indexed: false }
    ]
  },
  { 
    type: 'event', 
    name: 'Approval', 
    inputs: [
      { name: 'owner', type: 'address', indexed: true },
      { name: 'spender', type: 'address', indexed: true },
      { name: 'value', type: 'uint256', indexed: false }
    ]
  }
] as const

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

const program = Effect.gen(function* () {
  const usdc = yield* Contract(USDC_ADDRESS, multiEventAbi)

  const { transfers, approvals } = yield* Effect.all({
    transfers: usdc.getEvents('Transfer', { fromBlock: 18000000n, toBlock: 18000100n }),
    approvals: usdc.getEvents('Approval', { fromBlock: 18000000n, toBlock: 18000100n })
  })

  yield* Effect.log(`Found ${transfers.length} transfers and ${approvals.length} approvals`)

  return { transfers, approvals }
}).pipe(Effect.provide(ProviderLayer))

Error Handling

Handle common event query errors with Effect Schedule:
import { Effect, Schedule } from 'effect'
import { ContractEventError, withRetrySchedule } from 'voltaire-effect'

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

const robustQuery = Effect.gen(function* () {
  const usdc = yield* Contract(USDC_ADDRESS, transferAbi)

  return yield* usdc.getEvents('Transfer', {
    fromBlock: 18000000n,
    toBlock: 18010000n
  })
}).pipe(
  // Use Effect Schedule for retries
  withRetrySchedule(
    Schedule.exponential("500 millis").pipe(
      Schedule.jittered,
      Schedule.compose(Schedule.recurs(3))
    )
  ),
  Effect.catchTag('ContractEventError', (e) => {
    if (e.message.includes('block range')) {
      return Effect.log('Block range too large, reduce chunk size')
    }
    return Effect.fail(e)
  }),
  Effect.provide(ProviderLayer)
)

Using getLogs Directly

For more control, use the getLogs free function directly:
import { Effect, Layer } from 'effect'
import { getLogs, Provider, HttpTransport } from 'voltaire-effect'

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

const program = Effect.gen(function* () {
  // Transfer event topic: keccak256('Transfer(address,address,uint256)')
  const transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'

  const logs = yield* getLogs({
    address: USDC_ADDRESS,
    topics: [transferTopic],
    fromBlock: '0x112a880', // hex block number
    toBlock: 'latest'
  })

  return logs
}).pipe(Effect.provide(ProviderLayer))

See Also