The repeatable path from "this event isn't tracked" to a typed DecodedEvent.
Companion to Lesson 3.
topic0 (= keccak256 of the signature) is a map key → a LogDecoder object →
a typed DecodedEvent. Decoders parse; handlers mutate the graph.
TopicFooBar = crypto.Keccak256Hash([]byte("FooBar(address,uint256)"))The signature string must be the canonical form: no spaces, no arg names, tuples as
(t1,t2).
Verify with cast keccak "FooBar(address,uint256)".type fooBarDecoder struct{} func (d *fooBarDecoder) Decode(l ethtypes.Log, tx *types.RawTransaction) (*types.DecodedEvent, error) { if len(l.Topics) < 2 || len(l.Data) < 32 { // shape guard — see gotchas return nil, errSkipEvent } return &types.DecodedEvent{ Type: types.EventFooBar, TxHash: tx.Hash, LogIndex: l.Index, Contract: l.Address, Payload: &types.FooBarEvent{ Who: common.BytesToAddress(l.Topics[1].Bytes()), // indexed → topic Amount: new(big.Int).SetBytes(l.Data[:32]), // non-indexed → data }, }, nil }
NewRegistry (pkg/decoder/decoder.go):
r.Register(TopicFooBar, &fooBarDecoder{})
Forgetting this is the #1 "my decoder does nothing" bug — the type is fine but it's not in the map.| Also add | Where | Why |
|---|---|---|
Payload struct (FooBarEvent) + EventFooBar type | pkg/types/event_payloads.go, events.go | The typed shape the decoder emits and the handler consumes. |
| A handler that turns the event into edge mutations | pkg/indexer/ | Decoders don't write to Memgraph — handlers do (Lesson 4). |
| Tests | alongside, *_test.go | Repo is test-first; a decoder PR without a decode test won't pass review. |
| Arg kind | Where it lives | How to read it |
|---|---|---|
address indexed | l.Topics[n] | common.BytesToAddress(l.Topics[n].Bytes()) |
uint256 indexed | l.Topics[n] | new(big.Int).SetBytes(l.Topics[n].Bytes()) |
uint256 (non-indexed) | l.Data[off:off+32] | new(big.Int).SetBytes(l.Data[off:off+32]) |
address (non-indexed) | l.Data[off+12:off+32] | last 20 bytes of the 32-byte word |
Indexed args fill Topics[1..3] in order; non-indexed args are ABI-packed left-to-right in Data, 32 bytes each.
Transfer hash identically. ERC-721 = 4 topics + empty data; ERC-20 = 3 topics + 32-byte data. Guard with len(l.Topics) < 3 || len(l.Data) < 32.Supply/Borrow/Withdraw with different signatures → different topic0s → separate TopicXxx constants. Don't reuse.CreateMarket(bytes32,(address,address,address,address,uint256)). Get one char wrong and topic0 won't match real logs — verify against an on-chain log.errSkipEvent, not a hard error, for "not for us" logs — DecodeBlock treats real errors as decode failures (debug-logged), skips are silent.FORTA-XXXX Linear ticket exists before the first edit · PR references it · make test && make lint + gofumpt -w before every commit · never push to main.Source of truth: pkg/decoder/decoder.go, pkg/decoder/topics.go, pkg/types/event_payloads.go.
Copy from the 20+ live r.Register(...) calls in NewRegistry. ⌘P → Save as PDF for a printable card.