Query Historical Events
UseContract.getEvents for historical event queries:
Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
// 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:Copy
Ask AI
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:Copy
Ask AI
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 thegetLogs free function directly:
Copy
Ask AI
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
- Contract Factory — Type-safe contract interactions
- Provider Service — Low-level
getLogsmethod - Block Streaming — Stream blocks for live watching
- ERC Standards — Event decoding utilities for ERC-20/721/1155
- Voltaire Stream — Core streaming documentation
- Effect Stream — Effect Stream API

