The eventdecoder package provides powerful tools for decoding smart contract event logs into structured, readable data. It includes built-in support for common TRC20 events, hundreds of common events and allows runtime registration of custom contract ABIs.
This document is part of the TronLib learning path:
- Quick Start Guide - Basic usage
- Architecture Overview - Understanding the design
- Event Decoder Package Reference (this document) - Event log processing
- Other Package Documentation - Additional functionality
- API Reference - Complete function documentation
The eventdecoder package features:
- Built-in TRC20 Support - Pre-registered Transfer and Approval events
- Runtime ABI Registration - Add custom contract ABIs dynamically
- Structured Output - Convert raw logs into typed event data
- Graceful Fallback - Handle unknown events without errors
- Signature Caching - Efficient lookup of event signatures
- Multi-Contract Support - Decode events from multiple contracts in one transaction
type DecodedEvent struct {
EventName string
Signature string
Parameters []EventParameter
}
type EventParameter struct {
Name string
Type string
Value interface{}
Indexed bool
}The package automatically includes common TRC20 event signatures:
Transfer(address indexed from, address indexed to, uint256 value)Approval(address indexed owner, address indexed spender, uint256 value)
package main
import (
"encoding/hex"
"fmt"
"log"
"github.com/kslamph/tronlib/pkg/eventdecoder"
"github.com/kslamph/tronlib/pkg/types"
)
func main() {
// Example: TRC20 Transfer event log data
// This data would typically come from transaction logs
// Transfer event signature hash
transferSig, _ := hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
// Indexed parameters (from, to addresses)
fromTopic, _ := hex.DecodeString("000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
toTopic, _ := hex.DecodeString("0000000000000000000000004e83362442b8d1bec281594cea3050c8eb01311c")
// Non-indexed data (amount)
amountData, _ := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000003e8") // 1000
topics := [][]byte{transferSig, fromTopic, toTopic}
// Decode the event
event, err := eventdecoder.DecodeLog(topics, amountData)
if err != nil {
log.Fatalf("Failed to decode event: %v", err)
}
// Print decoded event
fmt.Printf("Event: %s\n", event.EventName)
fmt.Printf("Signature: %s\n", event.Signature)
for _, param := range event.Parameters {
fmt.Printf(" %s (%s): %v\n", param.Name, param.Type, param.Value)
}
// Output:
// Event: Transfer
// Signature: Transfer(address,address,uint256)
// from (address): 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
// to (address): 0x4e83362442b8d1bec281594cea3050c8eb01311c
// value (uint256): 1000
}// Decode events from a transaction result
func DecodeTransactionEvents(result *client.BroadcastResult) {
if len(result.Logs) == 0 {
fmt.Println("No events emitted")
return
}
fmt.Printf("Transaction %s emitted %d events:\n", result.TxID, len(result.Logs))
for i, log := range result.Logs {
// Get contract address that emitted the event
contractAddr := types.MustNewAddressFromBytes(log.GetAddress())
// Decode the event
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
fmt.Printf(" [%d] Failed to decode event from %s: %v\n", i, contractAddr, err)
continue
}
fmt.Printf(" [%d] %s.%s:\n", i, contractAddr, event.EventName)
for _, param := range event.Parameters {
indexedStr := ""
if param.Indexed {
indexedStr = " (indexed)"
}
fmt.Printf(" %s%s: %v\n", param.Name, indexedStr, param.Value)
}
}
}
// Usage after a contract transaction
result, err := cli.SignAndBroadcast(ctx, tx, opts, signer)
if err == nil {
DecodeTransactionEvents(result)
}// Register a custom contract ABI for event decoding
customABI := `{
"entrys": [
{
"type": "event",
"name": "UserRegistered",
"inputs": [
{"name": "user", "type": "address", "indexed": true},
{"name": "email", "type": "string", "indexed": false},
{"name": "timestamp", "type": "uint256", "indexed": false}
]
},
{
"type": "event",
"name": "ProfileUpdated",
"inputs": [
{"name": "user", "type": "address", "indexed": true},
{"name": "field", "type": "string", "indexed": true},
{"name": "oldValue", "type": "string", "indexed": false},
{"name": "newValue", "type": "string", "indexed": false}
]
}
]
}`
// Register the ABI
err := eventdecoder.RegisterABIJSON(customABI)
if err != nil {
log.Fatalf("Failed to register ABI: %v", err)
}
fmt.Println("✅ Custom ABI registered successfully")
// Now events from this contract will be automatically decoded// If you already have a parsed ABI object
var abiObject core.SmartContract_ABI
err := json.Unmarshal([]byte(abiJSON), &abiObject)
if err != nil {
log.Fatal(err)
}
// Register the ABI object directly
err = eventdecoder.RegisterABIObject(&abiObject)
if err != nil {
log.Fatalf("Failed to register ABI object: %v", err)
}// Register multiple contract ABIs at once
type ContractABI struct {
Name string
ABI string
}
func RegisterContractABIs(abis []ContractABI) error {
for _, contract := range abis {
fmt.Printf("Registering ABI for %s...\n", contract.Name)
err := eventdecoder.RegisterABIJSON(contract.ABI)
if err != nil {
return fmt.Errorf("failed to register %s ABI: %w", contract.Name, err)
}
}
fmt.Printf("✅ Successfully registered %d contract ABIs\n", len(abis))
return nil
}
// Usage
contractABIs := []ContractABI{
{"DEX", dexABI},
{"Lending", lendingABI},
{"Governance", governanceABI},
}
err := RegisterContractABIs(contractABIs)
if err != nil {
log.Fatal(err)
}// Filter events by type from transaction logs
func FilterEventsByType(logs []*core.TransactionInfo_Log, eventType string) []eventdecoder.DecodedEvent {
var filteredEvents []eventdecoder.DecodedEvent
for _, log := range logs {
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
continue // Skip events that can't be decoded
}
if event.EventName == eventType {
filteredEvents = append(filteredEvents, *event)
}
}
return filteredEvents
}
// Usage
transferEvents := FilterEventsByType(result.Logs, "Transfer")
approvalEvents := FilterEventsByType(result.Logs, "Approval")
fmt.Printf("Found %d Transfer events and %d Approval events\n",
len(transferEvents), len(approvalEvents))// Process events from specific contracts
func ProcessEventsByContract(logs []*core.TransactionInfo_Log, contractAddresses []*types.Address) map[string][]eventdecoder.DecodedEvent {
eventsByContract := make(map[string][]eventdecoder.DecodedEvent)
// Create address lookup map
addressMap := make(map[string]bool)
for _, addr := range contractAddresses {
addressMap[addr.Hex()] = true
}
for _, log := range logs {
// Check if event is from one of our target contracts
logAddr := types.MustNewAddressFromBytes(log.GetAddress())
if !addressMap[logAddr.Hex()] {
continue
}
// Decode event
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
continue
}
// Group by contract address
addrStr := logAddr.String()
eventsByContract[addrStr] = append(eventsByContract[addrStr], *event)
}
return eventsByContract
}
// Usage
contracts := []*types.Address{
types.MustNewAddressFromBase58("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"), // USDT
types.MustNewAddressFromBase58("TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8"), // USDC
}
eventsByContract := ProcessEventsByContract(result.Logs, contracts)
for contractAddr, events := range eventsByContract {
fmt.Printf("Contract %s emitted %d events:\n", contractAddr, len(events))
for _, event := range events {
fmt.Printf(" - %s\n", event.EventName)
}
}// Process events in real-time from new transactions
func ProcessNewTransactionEvents(ctx context.Context, cli *client.Client, contractAddr *types.Address) {
// This is a conceptual example - you'd need to implement transaction monitoring
for {
select {
case <-ctx.Done():
return
default:
// Get latest transactions (implementation specific)
transactions := getLatestTransactions(cli, contractAddr)
for _, tx := range transactions {
// Get transaction info with logs
txInfo, err := cli.GetTransactionInfo(ctx, tx.TxID)
if err != nil {
continue
}
// Process events
for _, log := range txInfo.GetLog() {
// Only process events from our contract
logAddr := types.MustNewAddressFromBytes(log.GetAddress())
if !logAddr.Equal(contractAddr) {
continue
}
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
fmt.Printf("Failed to decode event: %v\n", err)
continue
}
// Handle specific event types
switch event.EventName {
case "Transfer":
handleTransferEvent(event)
case "Approval":
handleApprovalEvent(event)
default:
fmt.Printf("Unknown event: %s\n", event.EventName)
}
}
}
time.Sleep(5 * time.Second) // Poll every 5 seconds
}
}
}
func handleTransferEvent(event *eventdecoder.DecodedEvent) {
// Extract transfer details
var from, to string
var amount *big.Int
for _, param := range event.Parameters {
switch param.Name {
case "from":
from = param.Value.(string)
case "to":
to = param.Value.(string)
case "value":
amount = param.Value.(*big.Int)
}
}
fmt.Printf("🔄 Transfer: %s → %s (Amount: %s)\n", from, to, amount.String())
}// Generate a human-readable summary of transaction events
func GenerateTransactionSummary(logs []*core.TransactionInfo_Log) string {
var summary strings.Builder
eventCounts := make(map[string]int)
summary.WriteString("Transaction Summary:\n")
for _, log := range logs {
contractAddr := types.MustNewAddressFromBytes(log.GetAddress())
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
summary.WriteString(fmt.Sprintf(" • Unknown event from %s\n", contractAddr))
continue
}
eventCounts[event.EventName]++
switch event.EventName {
case "Transfer":
summary.WriteString(generateTransferSummary(event, contractAddr))
case "Approval":
summary.WriteString(generateApprovalSummary(event, contractAddr))
default:
summary.WriteString(fmt.Sprintf(" • %s event from %s\n", event.EventName, contractAddr))
}
}
// Add event counts
summary.WriteString("\nEvent Counts:\n")
for eventType, count := range eventCounts {
summary.WriteString(fmt.Sprintf(" %s: %d\n", eventType, count))
}
return summary.String()
}
func generateTransferSummary(event *eventdecoder.DecodedEvent, contractAddr *types.Address) string {
var from, to, amount string
for _, param := range event.Parameters {
switch param.Name {
case "from":
from = param.Value.(string)
case "to":
to = param.Value.(string)
case "value":
amount = param.Value.(*big.Int).String()
}
}
return fmt.Sprintf(" 💸 %s tokens transferred from %s to %s\n", amount, from[:10]+"...", to[:10]+"...")
}
func generateApprovalSummary(event *eventdecoder.DecodedEvent, contractAddr *types.Address) string {
var owner, spender, amount string
for _, param := range event.Parameters {
switch param.Name {
case "owner":
owner = param.Value.(string)
case "spender":
spender = param.Value.(string)
case "value":
amount = param.Value.(*big.Int).String()
}
}
return fmt.Sprintf(" ✅ %s approved %s tokens for %s\n", owner[:10]+"...", amount, spender[:10]+"...")
}// Analyze events for insights
type EventAnalytics struct {
TotalEvents int
EventTypes map[string]int
ContractCounts map[string]int
TransferVolume *big.Int
UniqueAddresses map[string]bool
}
func AnalyzeEvents(logs []*core.TransactionInfo_Log) *EventAnalytics {
analytics := &EventAnalytics{
EventTypes: make(map[string]int),
ContractCounts: make(map[string]int),
TransferVolume: big.NewInt(0),
UniqueAddresses: make(map[string]bool),
}
for _, log := range logs {
contractAddr := types.MustNewAddressFromBytes(log.GetAddress())
analytics.ContractCounts[contractAddr.String()]++
analytics.TotalEvents++
event, err := eventdecoder.DecodeLog(log.GetTopics(), log.GetData())
if err != nil {
analytics.EventTypes["Unknown"]++
continue
}
analytics.EventTypes[event.EventName]++
// Special handling for Transfer events
if event.EventName == "Transfer" {
for _, param := range event.Parameters {
switch param.Name {
case "from", "to":
if addr, ok := param.Value.(string); ok {
analytics.UniqueAddresses[addr] = true
}
case "value":
if amount, ok := param.Value.(*big.Int); ok {
analytics.TransferVolume.Add(analytics.TransferVolume, amount)
}
}
}
}
}
return analytics
}
func (a *EventAnalytics) PrintSummary() {
fmt.Printf("📊 Event Analytics Summary:\n")
fmt.Printf("Total Events: %d\n", a.TotalEvents)
fmt.Printf("Unique Addresses: %d\n", len(a.UniqueAddresses))
fmt.Printf("Total Transfer Volume: %s\n", a.TransferVolume.String())
fmt.Println("\nEvent Types:")
for eventType, count := range a.EventTypes {
fmt.Printf(" %s: %d\n", eventType, count)
}
fmt.Println("\nContracts:")
for contract, count := range a.ContractCounts {
fmt.Printf(" %s: %d events\n", contract, count)
}
}// Generate event signature for manual lookup
func GenerateEventSignature(eventName string, paramTypes []string) []byte {
signature := fmt.Sprintf("%s(%s)", eventName, strings.Join(paramTypes, ","))
hash := crypto.Keccak256Hash([]byte(signature))
return hash.Bytes()
}
// Check if event signature is registered
func IsEventRegistered(topics [][]byte) bool {
if len(topics) == 0 {
return false
}
// Try to decode - if successful, it's registered
_, err := eventdecoder.DecodeLog(topics, []byte{})
return err == nil
}
// Get all registered event signatures
func GetRegisteredSignatures() map[string]string {
// This would need to be implemented in the eventdecoder package
// Return map of signature -> event name
return map[string]string{
"ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": "Transfer(address,address,uint256)",
"8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": "Approval(address,address,uint256)",
}
}// Validate event structure
func ValidateEvent(event *eventdecoder.DecodedEvent) error {
if event.EventName == "" {
return errors.New("event name cannot be empty")
}
if len(event.Parameters) == 0 {
return errors.New("event must have at least one parameter")
}
// Validate parameter structure
for i, param := range event.Parameters {
if param.Name == "" {
return fmt.Errorf("parameter %d has empty name", i)
}
if param.Type == "" {
return fmt.Errorf("parameter %d (%s) has empty type", i, param.Name)
}
if param.Value == nil {
return fmt.Errorf("parameter %d (%s) has nil value", i, param.Name)
}
}
return nil
}
// Validate event against expected structure
func ValidateEventStructure(event *eventdecoder.DecodedEvent, expectedName string, expectedParams []string) error {
if event.EventName != expectedName {
return fmt.Errorf("expected event %s, got %s", expectedName, event.EventName)
}
if len(event.Parameters) != len(expectedParams) {
return fmt.Errorf("expected %d parameters, got %d", len(expectedParams), len(event.Parameters))
}
for i, expectedParam := range expectedParams {
if event.Parameters[i].Name != expectedParam {
return fmt.Errorf("parameter %d: expected %s, got %s", i, expectedParam, event.Parameters[i].Name)
}
}
return nil
}// Handle different types of decoding errors
func SafeDecodeEvent(topics [][]byte, data []byte) (*eventdecoder.DecodedEvent, error) {
event, err := eventdecoder.DecodeLog(topics, data)
if err != nil {
// Check for specific error types
if strings.Contains(err.Error(), "unknown signature") {
// Event signature not registered
return &eventdecoder.DecodedEvent{
EventName: "UnknownEvent",
Signature: hex.EncodeToString(topics[0]),
Parameters: []eventdecoder.EventParameter{},
}, nil
}
if strings.Contains(err.Error(), "invalid data length") {
return nil, fmt.Errorf("event data corrupted: %w", err)
}
if strings.Contains(err.Error(), "invalid topic count") {
return nil, fmt.Errorf("event topics malformed: %w", err)
}
return nil, fmt.Errorf("unknown decoding error: %w", err)
}
return event, nil
}func TestEventDecoding(t *testing.T) {
// Test TRC20 Transfer event
transferSig, _ := hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
fromTopic, _ := hex.DecodeString("000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
toTopic, _ := hex.DecodeString("0000000000000000000000004e83362442b8d1bec281594cea3050c8eb01311c")
amountData, _ := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000003e8")
topics := [][]byte{transferSig, fromTopic, toTopic}
event, err := eventdecoder.DecodeLog(topics, amountData)
require.NoError(t, err)
require.Equal(t, "Transfer", event.EventName)
require.Len(t, event.Parameters, 3)
// Validate parameters
assert.Equal(t, "from", event.Parameters[0].Name)
assert.Equal(t, "to", event.Parameters[1].Name)
assert.Equal(t, "value", event.Parameters[2].Name)
}
func TestCustomEventRegistration(t *testing.T) {
customABI := `{"entrys":[{"type":"event","name":"CustomEvent","inputs":[{"name":"param","type":"uint256","indexed":false}]}]}`
err := eventdecoder.RegisterABIJSON(customABI)
require.NoError(t, err)
// Test that custom event can now be decoded
// (You would need to construct proper test data)
}// Generate mock events for testing
func GenerateMockTransferEvent(from, to *types.Address, amount *big.Int) ([][]byte, []byte) {
// Transfer signature
transferSig, _ := hex.DecodeString("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
// Encode addresses as topics
fromTopic := make([]byte, 32)
copy(fromTopic[12:], from.BytesEVM())
toTopic := make([]byte, 32)
copy(toTopic[12:], to.BytesEVM())
topics := [][]byte{transferSig, fromTopic, toTopic}
// Encode amount as data
data := make([]byte, 32)
amount.FillBytes(data)
return topics, data
}
// Usage in tests
func TestTransferEventDecoding(t *testing.T) {
from := types.MustNewAddressFromBase58("TLyqzVGLV1srkB7dToTAEqgDSfPtXRJZYH")
to := types.MustNewAddressFromBase58("TBkfmcE7pM8cwxEhATtkMFwAf1FeQcwY9x")
amount := big.NewInt(1000)
topics, data := GenerateMockTransferEvent(from, to, amount)
event, err := eventdecoder.DecodeLog(topics, data)
require.NoError(t, err)
require.Equal(t, "Transfer", event.EventName)
}The eventdecoder package transforms raw blockchain event logs into meaningful, structured data. Use these patterns to build rich event-driven applications and analytics tools! 🚀