Skip to main content

Overview

TopicFilter is an array-based filter for matching event topics (indexed parameters) in Ethereum logs. Topics use AND logic across positions and OR logic within arrays, enabling precise event filtering for applications like DEX trackers and wallet monitors.

Type Definition

type TopicEntry = HashType | readonly HashType[] | null;

type TopicFilterType = readonly [
  TopicEntry?,
  TopicEntry?,
  TopicEntry?,
  TopicEntry?
] & {
  readonly [brand]: "TopicFilter";
};

Topic Positions

Events can have up to 4 topics (0-3):
  • topic0: Event signature hash (required for non-anonymous events)
  • topic1-3: Indexed parameters (if declared with indexed in Solidity)
event Transfer(address indexed from, address indexed to, uint256 value);
//              topic1             topic2           (not indexed, in data)

Creating TopicFilter

from

import * as TopicFilter from './primitives/TopicFilter/index.js';
import { Hash } from './primitives/Hash/index.js';

// Match specific event
const filter = TopicFilter.from([eventSigHash]);

// Match with wildcards
const filter2 = TopicFilter.from([eventSigHash, null, recipientHash]);

// Match any of multiple values (OR)
const filter3 = TopicFilter.from([[eventSig1, eventSig2]]);
Parameters:
  • topics: Array of up to 4 entries, each being:
    • HashType - Match this specific hash
    • HashType[] - Match any of these hashes (OR)
    • null - Wildcard, matches anything
Returns: TopicFilterType Throws: InvalidTopicFilterError if array has >4 entries or invalid hashes

Operations

matches

const isMatch = TopicFilter.matches(filter, log.topics);
Checks if a log’s topics match the filter criteria. Uses:
  • AND logic across positions: All non-null positions must match
  • OR logic within arrays: Any hash in array matches
Examples:
// Filter: [eventSig, null, recipientHash]
TopicFilter.matches(filter, [eventSig, senderHash, recipientHash]); // true
TopicFilter.matches(filter, [eventSig, anyHash, recipientHash]);    // true
TopicFilter.matches(filter, [eventSig, anyHash, wrongHash]);        // false

// Filter: [[eventSig1, eventSig2], null]
TopicFilter.matches(filter, [eventSig1, anyHash]); // true
TopicFilter.matches(filter, [eventSig2, anyHash]); // true
TopicFilter.matches(filter, [eventSig3, anyHash]); // false

isEmpty

const empty = TopicFilter.isEmpty(filter);
Returns true if all positions are null or undefined (no filtering).

Matching Logic

Position-based AND

// ALL non-null positions must match
const filter = TopicFilter.from([hash1, hash2, hash3]);

// Matches: topics must be [hash1, hash2, hash3]
// Does NOT match: [hash1, hash2, wrongHash]

Array-based OR

// ANY hash in array matches
const filter = TopicFilter.from([[hash1, hash2]]);

// Matches: [hash1, ...] OR [hash2, ...]
// Does NOT match: [hash3, ...]

Wildcards

// null matches anything
const filter = TopicFilter.from([hash1, null, hash3]);

// Matches: [hash1, ANYTHING, hash3]

Common Patterns

Specific Event

import { EventSignature } from './primitives/EventSignature/index.js';

// Transfer(address,address,uint256)
const transferSig = EventSignature.fromSignature(
  "Transfer(address,address,uint256)"
);
const filter = TopicFilter.from([transferSig]);

Event with Specific Sender

const senderHash = Address.from("0x...").toBytes32();
const filter = TopicFilter.from([transferSig, senderHash]);

Event with Specific Recipient

const recipientHash = Address.from("0x...").toBytes32();
const filter = TopicFilter.from([transferSig, null, recipientHash]);

Multiple Event Types

// Match Transfer OR Approval
const filter = TopicFilter.from([[transferSig, approvalSig]]);

Transactions Between Two Addresses

// From addr1 OR to addr1
const addr1Hash = Address.from("0x...").toBytes32();
const filter = TopicFilter.from([
  transferSig,
  [addr1Hash], // from addr1
  [addr1Hash]  // OR to addr1
]);

Bloom Filters

Ethereum blocks contain bloom filters that enable efficient topic matching without scanning all logs:
// Node first checks bloom filter
if (block.logsBloom.mightContainTopic(topic)) {
  // Only then scan actual logs
  const logs = block.logs.filter(log =>
    TopicFilter.matches(filter, log.topics)
  );
}
Bloom filter properties:
  • Probabilistic data structure (false positives possible, no false negatives)
  • 2048-bit (256-byte) bit array
  • 3 hash functions per topic
  • Enables fast block scanning for eth_getLogs

Example: DEX Trade Monitor

import * as TopicFilter from './primitives/TopicFilter/index.js';
import * as LogFilter from './primitives/LogFilter/index.js';
import { EventSignature } from './primitives/EventSignature/index.js';
import { Address } from './primitives/Address/index.js';

// Uniswap V2 Swap event
const swapSig = EventSignature.fromSignature(
  "Swap(address,uint256,uint256,uint256,uint256,address)"
);

// WETH address as topic (if indexed)
const wethAddr = Address.from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
const wethHash = wethAddr.toBytes32();

// Match swaps involving WETH
const topics = TopicFilter.from([
  swapSig,
  null,              // any sender
  [wethHash, null]   // WETH as token0 OR token1
]);

const filter = LogFilter.from({
  fromBlock: "latest",
  topics
});

// Poll for new swaps
const logs = await rpc.eth_getLogs(filter);

Performance Considerations

Bloom Filter Optimization

More specific filters = faster queries:
// FAST: Specific event + address
const filter1 = TopicFilter.from([eventSig, addrHash]);

// SLOWER: Event only (many addresses to check)
const filter2 = TopicFilter.from([eventSig]);

// SLOWEST: Wildcard (bloom filter can't help)
const filter3 = TopicFilter.from([null]);

Node Resource Limits

Nodes may limit:
  • Query block range (e.g., 10,000 blocks max)
  • Number of results (e.g., 10,000 logs max)
  • Query complexity (multiple OR conditions)
Best practices:
  • Use smallest block ranges possible
  • Filter by contract address when possible
  • Add topic filters to reduce result set
  • Paginate large queries by block range

See Also