The Decoder is a pluggable component responsible for transforming raw blockchain event logs into structured, typed events. It operates independently of the Processor, allowing users to either use the provided StandardDecoder implementation or implement custom decoding logic.
- Parse ABI (Application Binary Interface) definitions
- Store ABIs with unique identifiers for explicit selection
- Match raw logs to event definitions via topic hash lookup
- Decode indexed parameters from log topics
- Decode non-indexed parameters from log data field
- Produce structured Event objects with typed field access
The Decoder interface defines the contract for transforming raw blockchain logs into structured events:
type Decoder interface {
// Decode transforms a single log into a structured event
Decode(name string, chainId string, log types.Log) (*types.Event, error)
// DecodeBatch processes multiple logs efficiently
DecodeBatch(logs []types.Log) (*[]types.Event, error)
}Methods:
Decode(name, chainId, log): Transforms a single log using specified ABI identifier and chain contextDecodeBatch(logs): Processes multiple logs efficiently (may use batch optimizations)
Events represent the decoded output with standardized metadata and flexible field storage:
type Event struct {
// Blockchain Metadata
ChainID string
BlockNumber uint64
BlockHash string
TxHash string
TxIndex uint64
LogIndex uint64
// Contract Information
Address string
// Event Identification
EventType string // "Transfer", "Approval", etc.
EventSig string // "Transfer(address,address,uint256)"
TopicHash string // Keccak256 hash of signature
// Decoded Fields
Fields EventFields
// Raw Log Reference
Raw *types.Log
}
type EventFields map[string]interface{}EventFields provides typed accessor methods:
GetAddress(key string) (string, error)GetUint64(key string) (uint64, error)GetBigInt(key string) (*big.Int, error)GetBool(key string) (bool, error)GetBytes(key string) ([]byte, error)GetString(key string) (string, error)
StandardDecoder is the default ABI-based implementation provided by the SDK.
Internal Structure:
type StandardDecoder struct {
events map[string]map[string]*EventDefinition // ABI name → topic hash → event definition
}Configuration Methods:
RegisterABI(name string, abiJSON string) error- Parse and register all events from ABI JSON with a unique identifierRegisterABIFromFile(name string, filepath string) error- Load ABI from file with a unique identifier
Each ABI must be registered with a unique identifier (name). This identifier is used to explicitly select which ABI to use when decoding logs. This design allows multiple ABIs with overlapping event signatures (e.g., ERC20 and ERC721 Transfer events) to coexist.
Why Identifiers are Required:
- Explicit Control: Users explicitly choose which ABI to use for decoding, avoiding ambiguity
- Multiple Variants: Different contracts may emit events with the same signature but different structures (e.g., ERC20 Transfer has 3 topics, ERC721 Transfer has 4 topics)
- Clear Intent: Makes it obvious which ABI is being used for each decode operation
- Performance: Direct lookup by name and topic hash (O(1)) without iteration
Registration Example:
decoder := core.NewStandardDecoder()
// Register ERC20 ABI with identifier "ERC20"
decoder.RegisterABI("ERC20", erc20ABI)
// Register ERC721 ABI with identifier "ERC721"
decoder.RegisterABI("ERC721", erc721ABI)
// Register custom contract ABI
decoder.RegisterABI("MyContract", myContractABI)Decoding with Identifier:
// Decode using ERC20 ABI for Ethereum chain
event, err := decoder.Decode("ERC20", "1", log)
// Decode using ERC721 ABI for Polygon chain
event, err := decoder.Decode("ERC721", "137", log)
// Batch decode (ABI selection handled internally)
events, err := decoder.DecodeBatch(logs)The decoder requires full event definitions, not just signatures. Each event definition must include:
- Event name - Human-readable identifier
- Parameter names - Used as keys in Event.Fields map
- Parameter types - Determines decoding logic (address, uint256, etc.)
- Indexed status - Identifies which parameters are in topics vs data field
Example ABI Event Definition:
{
"name": "Transfer",
"type": "event",
"inputs": [
{"name": "from", "type": "address", "indexed": true},
{"name": "to", "type": "address", "indexed": true},
{"name": "value", "type": "uint256", "indexed": false}
]
}- ABI Selection: User specifies which ABI identifier to use via
DecodeWith(name, log) - Topic Hash Lookup: Extract
topics[0]and lookup event definition in the specified ABI registry - Structure Validation: Verify log structure matches event definition (topic count, data presence)
- Indexed Parameter Decoding: Decode
topics[1..n]based on indexed parameter types - Data Field Decoding: Parse log data field for non-indexed parameters
- Field Population: Combine decoded values into EventFields map
- Event Construction: Return structured Event with metadata and fields
| Solidity Type | Go Type | Storage |
|---|---|---|
| address | string | topics or data |
| uint8-uint64 | uint64 | topics or data |
| uint256 | *big.Int | topics or data |
| int8-int64 | int64 | topics or data |
| int256 | *big.Int | topics or data |
| bool | bool | topics or data |
| bytes, bytesN | []byte | data only |
| string | string | data only |
| arrays | []T | data only |
Different contracts may emit events with the same signature but different structures. For example:
- ERC20 Transfer:
Transfer(address indexed, address indexed, uint256)- 3 topics, value in data - ERC721 Transfer:
Transfer(address indexed, address indexed, uint256 indexed)- 4 topics, tokenId in topics
Both have the same topic hash (0xddf252ad...) but different structures. By registering each with a unique identifier, users can explicitly choose which variant to use:
decoder.RegisterABI("ERC20", erc20TransferABI) // 3 topics expected
decoder.RegisterABI("ERC721", erc721TransferABI) // 4 topics expected
// User explicitly selects which variant to use
event, err := decoder.DecodeWith("ERC20", log) // Uses ERC20 structure
event, err := decoder.DecodeWith("ERC721", log) // Uses ERC721 structureStandardDecoder is chain-agnostic and can be shared across multiple EVM chains:
decoder := core.NewStandardDecoder()
decoder.RegisterABI("ERC20", erc20ABI) // Works on all EVM chains
processor.AddChain(ethereumChain, &Options{Decoder: decoder})
processor.AddChain(polygonChain, &Options{Decoder: decoder})Rationale: Standard Solidity events use identical encoding across all EVM-compatible chains.
A single decoder can handle multiple contracts with different ABIs, each registered with a unique identifier:
decoder := core.NewStandardDecoder()
decoder.RegisterABI("USDC", usdcABI) // ERC20 events
decoder.RegisterABI("Uniswap", uniswapABI) // DEX events
decoder.RegisterABI("NFT", nftABI) // ERC721 events
// Decode with explicit ABI selection
usdcEvent, _ := decoder.DecodeWith("USDC", log)
uniswapEvent, _ := decoder.DecodeWith("Uniswap", log)The DecoderRouter provides intelligent routing of blockchain logs to appropriate decoders based on configurable matching conditions. It enables complex decoding scenarios where different contracts or event types require different ABI interpretations.
The DecoderRouter implements the same Decoder interface but internally manages multiple decoder instances and routes logs based on match conditions.
Core Components:
type DecoderRouter struct {
routes []DecoderRoute
}
type DecoderRoute struct {
Match MatchFunc // Condition for routing
Name string // ABI identifier for decoder
Decoder Decoder // Target decoder instance
}
type MatchFunc func(log types.Log) boolThe SDK provides several built-in match functions for common routing scenarios:
ByTopicCount(count int) Matches logs with exactly N topics. Useful for distinguishing event variants:
// ERC20 Transfer: 3 topics (Transfer(address,address,uint256))
router.Register(ByTopicCount(3), "ERC20", decoder)
// ERC721 Transfer: 4 topics (Transfer(address,address,uint256,uint256))
router.Register(ByTopicCount(4), "ERC721", decoder)ByAddress(address string) Matches logs from a specific contract address:
router.Register(ByAddress("0xA0b86a33E6441e88C5F2712C3E9b74Ec6F6FDD6F"), "UniswapV2", decoder)ByAddresses(addresses []string) Matches logs from any address in the provided list:
router.Register(ByAddresses([]string{
"0xTokenA",
"0xTokenB",
"0xTokenC",
}), "ERC20", decoder)ByTopic0(topic0 string) Matches logs by event signature (first topic hash):
transferTopic := "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
router.Register(ByTopic0(transferTopic), "Transfer", decoder)And(matchers ...MatchFunc) Combines multiple matchers with AND logic (all conditions must be true):
router.Register(And(
ByTopicCount(4),
ByAddress("0xNFTContract"),
ByTopic0(transferTopic),
), "ERC721", decoder)Or(matchers ...MatchFunc) Combines multiple matchers with OR logic (any condition must be true):
router.Register(Or(
ByAddress("0xContractA"),
ByAddress("0xContractB"),
), "SpecialABI", decoder)Basic Setup:
// Create router instance
router := decoder.NewDecoderRouter()
// Register routes with match conditions
router.
Register(decoder.ByTopicCount(3), "ERC20", erc20Decoder).
Register(decoder.ByTopicCount(4), "ERC721", erc721Decoder).
Register(decoder.ByAddress("0xUniswap"), "DEX", dexDecoder)Complex Routing Example:
// ERC721: Must have 4 topics AND come from NFT contract AND be Transfer event
erc721Matcher := decoder.And(
decoder.ByTopicCount(4),
decoder.ByAddress("0xNFTContract"),
decoder.ByTopic0("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
)
// ERC20: Must have 3 topics AND be Transfer event (from any contract)
erc20Matcher := decoder.And(
decoder.ByTopicCount(3),
decoder.ByTopic0("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
)
// DEX: Must come from Uniswap OR PancakeSwap
dexMatcher := decoder.Or(
decoder.ByAddress("0xUniswapV2"),
decoder.ByAddress("0xPancakeSwap"),
)
router := decoder.NewDecoderRouter().
Register(erc721Matcher, "ERC721", erc721Decoder).
Register(erc20Matcher, "ERC20", erc20Decoder).
Register(dexMatcher, "DEX", dexDecoder)Route Evaluation:
- Routes are evaluated in registration order (first match wins)
- For each log, the router iterates through routes sequentially
- First route where
Match(log)returnstrueis selected - If no routes match, the log is skipped (returns
nil, nil)
Route Ordering Matters:
// Order matters - more specific routes first
router := decoder.NewDecoderRouter().
// Specific: Uniswap contract (checked first)
Register(ByAddress("0xUniswap"), "Uniswap", decoder).
// General: Any 3-topic Transfer (checked second)
Register(ByTopicCount(3), "ERC20", decoder)Fallback Routes:
// Catch-all route (nil matcher matches everything)
router.Register(nil, "DefaultABI", decoder)
// Specific routes override fallback
router.
Register(ByAddress("0xSpecial"), "SpecialABI", decoder).
Register(nil, "DefaultABI", decoder) // Fallback for non-special contractsThe DecoderRouter integrates seamlessly with the Processor, replacing single decoders:
// Create and configure router
router := decoder.NewDecoderRouter().
Register(decoder.ByTopicCount(3), "ERC20", standardDecoder).
Register(decoder.ByTopicCount(4), "ERC721", standardDecoder)
// Register with processor (same as single decoder)
processor.AddChain(chainInfo, options, router)Multi-Contract Indexing Example:
// Setup decoders with different ABIs
erc20Decoder := core.NewStandardDecoder()
erc20Decoder.RegisterABI("ERC20", erc20ABI)
erc721Decoder := core.NewStandardDecoder()
erc721Decoder.RegisterABI("ERC721", erc721ABI)
dexDecoder := core.NewStandardDecoder()
dexDecoder.RegisterABI("DEX", dexABI)
// Create router with intelligent routing
router := decoder.NewDecoderRouter().
// ERC721: 4 topics + specific NFT contracts
Register(
decoder.And(
decoder.ByTopicCount(4),
decoder.ByAddresses([]string{"0xAzuki", "0xBAYC", "0xMAYC"}),
),
"ERC721",
erc721Decoder,
).
// DEX: Specific DEX contracts
Register(
decoder.ByAddresses([]string{"0xUniswapV2", "0xSushiSwap"}),
"DEX",
dexDecoder,
).
// ERC20: Default for 3-topic transfers
Register(decoder.ByTopicCount(3), "ERC20", erc20Decoder)
// Register with processor
processor.AddChain(ethereumChain, options, router)Router Benefits:
- Flexibility: Handle multiple contract types in single indexer
- Performance: Route logs without iterating through all decoders
- Maintainability: Central routing logic instead of scattered conditionals
- Extensibility: Easy addition of new contract types and matchers
Users can implement custom match functions for specialized routing logic:
// Custom matcher: Block range filtering
func ByBlockRange(from, to uint64) decoder.MatchFunc {
return func(log types.Log) bool {
blockNum, _ := utils.HexQtyToUint64(log.BlockNumber)
return blockNum >= from && blockNum <= to
}
}
// Custom matcher: Data length filtering
func ByDataLength(minLength int) decoder.MatchFunc {
return func(log types.Log) bool {
return len(log.Data) >= minLength
}
}
// Usage
router.Register(
decoder.And(
ByBlockRange(1000000, 2000000), // Blocks 1M-2M
ByDataLength(32), // At least 32 bytes of data
),
"HistoricalABI",
decoder,
)Users can implement custom decoders for non-standard requirements:
Use Cases:
- Non-EVM chains with different encoding schemes
- Pre-filtering or post-processing logic
- Event enrichment with external data
- Performance optimization for known event structures
Implementation Requirements:
- Satisfy Decoder interface (Decode, DecodeBatch)
- Return Event objects with proper structure
- Handle errors gracefully
The Processor integrates decoders directly during event processing:
// Register decoder/router when adding chain
processor.AddChain(chain, options, decoder)
// Processor automatically:
// 1. Fetches logs via RPC
// 2. Calls decoder.Decode() for each log (router routes internally)
// 3. Stores resulting events via sinkKey Integration Points:
- Decoder registered per-chain during
AddChain() - Processor handles decoding internally for ordered, atomic processing
- Decoder errors logged but don't stop processing (resilient design)
- Router enables complex multi-contract scenarios