> ## Documentation Index
> Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Event Listening

> Subscribe to and query contract events

<Info>
  **Skill** — Copyable reference implementation. Use as-is or customize. See [Skills Philosophy](/concepts/skills).
</Info>

# Event Listening

This guide covers subscribing to live contract events and querying historical event logs using Voltaire's Contract and EventStream APIs.

## Prerequisites

```typescript theme={null}
import { Contract, EventStream } from '@voltaire/contract';
import type { TypedProvider } from '@voltaire/provider';
import * as S from 'effect/Schema';
import * as AbiSchema from 'voltaire-effect/primitives/Abi';

// ERC20 ABI with Transfer and Approval events
const erc20Abi = S.decodeUnknownSync(AbiSchema.fromArray)([
  {
    type: 'event',
    name: 'Transfer',
    inputs: [
      { type: 'address', name: 'from', indexed: true },
      { type: 'address', name: 'to', indexed: true },
      { type: 'uint256', name: 'value', indexed: false },
    ],
  },
  {
    type: 'event',
    name: 'Approval',
    inputs: [
      { type: 'address', name: 'owner', indexed: true },
      { type: 'address', name: 'spender', indexed: true },
      { type: 'uint256', name: 'value', indexed: false },
    ],
  },
]);
```

## Creating a Contract Instance

```typescript theme={null}
const usdc = Contract({
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  abi: erc20Abi,
  provider
});
```

## Subscribing to Live Events

Use `watch()` to poll for new events as they occur:

```typescript theme={null}
const stream = usdc.events.Transfer({});

for await (const { log, metadata } of stream.watch({
  pollingInterval: 2000  // Poll every 2 seconds (default)
})) {
  console.log('New transfer detected');
  console.log('From:', log.args.from);
  console.log('To:', log.args.to);
  console.log('Value:', log.args.value);
  console.log('Block:', metadata.currentBlock);
}
```

### Start Watching from a Specific Block

```typescript theme={null}
for await (const { log } of stream.watch({
  fromBlock: 19000000n
})) {
  processTransfer(log);
}
```

### Stop Watching with AbortSignal

```typescript theme={null}
import { EventStreamAbortedError } from '@voltaire/contract';

const controller = new AbortController();

// Stop after 30 seconds
setTimeout(() => controller.abort(), 30000);

try {
  for await (const { log } of stream.watch({
    signal: controller.signal
  })) {
    console.log('Transfer:', log.args.value);
  }
} catch (error) {
  if (error instanceof EventStreamAbortedError) {
    console.log('Stopped watching');
  }
}
```

## Querying Historical Events

Use `backfill()` to fetch events from a specific block range:

```typescript theme={null}
const stream = usdc.events.Transfer({});

for await (const { log, metadata } of stream.backfill({
  fromBlock: 18000000n,
  toBlock: 18001000n
})) {
  console.log(`Block ${log.blockNumber}: ${log.args.value} tokens`);
}
// Loop completes after processing all historical events
```

### Dynamic Chunking for Large Ranges

EventStream automatically handles large block ranges by chunking requests:

* Starts with 500 blocks per request (configurable)
* Reduces chunk size by 50% on "block range too large" errors
* Increases chunk size by 25% after 5 consecutive successes

```typescript theme={null}
for await (const { log } of stream.backfill({
  fromBlock: 0n,
  toBlock: 19000000n,
  chunkSize: 1000,      // Initial chunk size (default: 500)
  minChunkSize: 50      // Minimum after reduction (default: 10)
})) {
  processEvent(log);
}
```

### Backfill Then Watch Pattern

Process all historical events, then continue with live events:

```typescript theme={null}
const stream = usdc.events.Transfer({});
const currentBlock = 19500000n;

// First: get all historical events
for await (const { log } of stream.backfill({
  fromBlock: 0n,
  toBlock: currentBlock
})) {
  processEvent(log);
}

// Then: watch for new events starting from current block
for await (const { log } of stream.watch({
  fromBlock: currentBlock
})) {
  processEvent(log);
}
```

## Filtering by Topic

Filter events by indexed parameters when creating the stream:

### Filter by Sender

```typescript theme={null}
const stream = usdc.events.Transfer({
  from: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
});

for await (const { log } of stream.backfill({
  fromBlock: 18000000n,
  toBlock: 18100000n
})) {
  // Only transfers FROM the specified address
  console.log('Outgoing transfer:', log.args.value);
}
```

### Filter by Recipient

```typescript theme={null}
const stream = usdc.events.Transfer({
  to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
});
```

### Filter by Both Sender and Recipient

```typescript theme={null}
const stream = usdc.events.Transfer({
  from: '0xSenderAddress...',
  to: '0xReceiverAddress...'
});
```

## Decoding Event Data

Each yielded result contains the decoded log and metadata:

```typescript theme={null}
for await (const { log, metadata } of stream.backfill({
  fromBlock: 18000000n,
  toBlock: 18001000n
})) {
  // Decoded event log
  console.log('Event name:', log.eventName);        // 'Transfer'
  console.log('From:', log.args.from);              // AddressType
  console.log('To:', log.args.to);                  // AddressType
  console.log('Value:', log.args.value);            // bigint
  console.log('Block number:', log.blockNumber);    // BlockNumberType
  console.log('Block hash:', log.blockHash);        // HashType
  console.log('Tx hash:', log.transactionHash);     // TransactionHashType
  console.log('Log index:', log.logIndex);          // number

  // Block context metadata
  console.log('Current block height:', metadata.currentBlock);
  console.log('Query range:', metadata.fromBlock, '-', metadata.toBlock);
}
```

## Standalone EventStream

Create an EventStream without a Contract for more control:

```typescript theme={null}
import { EventStream } from '@voltaire/contract';

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

const stream = EventStream({
  provider,
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  event: transferEvent,
  filter: { from: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' }
});

for await (const { log } of stream.backfill({
  fromBlock: 18000000n,
  toBlock: 19000000n
})) {
  console.log(log.args);
}
```

## Retry Configuration

Configure retry behavior for transient RPC errors:

```typescript theme={null}
for await (const { log } of stream.backfill({
  fromBlock: 0n,
  toBlock: 1000000n,
  retry: {
    maxRetries: 5,         // Max retry attempts (default: 3)
    initialDelay: 1000,    // Initial delay ms (default: 1000)
    maxDelay: 30000        // Max delay ms with exponential backoff (default: 30000)
  }
})) {
  processEvent(log);
}
```

## Error Handling

```typescript theme={null}
import {
  BlockRangeTooLargeError,
  EventStreamAbortedError
} from '@voltaire/contract';

try {
  for await (const { log } of stream.backfill({
    fromBlock: 0n,
    toBlock: 1000000n
  })) {
    console.log(log);
  }
} catch (error) {
  if (error instanceof EventStreamAbortedError) {
    console.log('Stream was cancelled via AbortSignal');
  } else if (error instanceof BlockRangeTooLargeError) {
    console.log('Block range exceeded RPC limit');
  } else {
    console.error('Unexpected error:', error);
  }
}
```

## Low-Level: Using eth\_getLogs Directly

For maximum control, use the provider's `eth_getLogs` method directly:

```typescript theme={null}
import * as Hex from '@voltaire/primitives/Hex';
import { Keccak256 } from '@voltaire/crypto/Keccak256';

// Compute event signature hash
const transferSig = 'Transfer(address,address,uint256)';
const topicHash = Keccak256.hashString(transferSig);

const logs = await provider.request({
  method: 'eth_getLogs',
  params: [{
    address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    topics: [Hex.fromBytes(topicHash)],
    fromBlock: '0x1234',
    toBlock: 'latest'
  }]
});

for (const log of logs) {
  // Raw log data - decode manually or use Abi.decodeLog
  console.log('Topics:', log.topics);
  console.log('Data:', log.data);
}
```

## Collecting Events into an Array

Break out of the stream after collecting a set number of events:

```typescript theme={null}
const events: Array<{
  from: string;
  to: string;
  value: bigint;
  block: bigint;
}> = [];

for await (const { log } of stream.backfill({
  fromBlock: 18000000n,
  toBlock: 19000000n
})) {
  events.push({
    from: String(log.args.from),
    to: String(log.args.to),
    value: log.args.value as bigint,
    block: BigInt(log.blockNumber)
  });

  if (events.length >= 100) {
    break; // Stop after 100 events
  }
}

console.log(`Collected ${events.length} transfers`);
```

## Break on Condition

Stop streaming when a specific condition is met:

```typescript theme={null}
const threshold = 1000000n * 10n ** 6n; // 1M USDC (6 decimals)

for await (const { log } of stream.watch()) {
  if (log.args.value > threshold) {
    console.log('Large transfer detected!', log.args.value);
    break; // Stream cleanup happens automatically
  }
}
```

## Related

* [Contract Events Reference](/contract/events) - Full EventStream API
* [Contract Overview](/contract) - Contract module introduction
* [Logs & Filters](/jsonrpc-provider/eth-methods/logs-filters) - Low-level eth\_getLogs
