Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
# EVM Scanner 🚀

[![Go Report Card](https://goreportcard.com/badge/github.com/84hero/evm-scanner)](https://goreportcard.com/report/github.com/84hero/evm-scanner)
[![Build Status](https://github.com/84hero/evm-scanner/workflows/Test%20and%20Lint/badge.svg)](https://github.com/84hero/evm-scanner/actions)
[![GoDoc](https://godoc.org/github.com/84hero/evm-scanner?status.svg)](https://godoc.org/github.com/84hero/evm-scanner)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<p align="center">
<a href="https://github.com/84hero/evm-scanner/actions/workflows/test.yml">
<img src="https://github.com/84hero/evm-scanner/actions/workflows/test.yml/badge.svg" alt="Build Status">
</a>
<a href="https://goreportcard.com/report/github.com/84hero/evm-scanner">
<img src="https://goreportcard.com/badge/github.com/84hero/evm-scanner" alt="Go Report Card">
</a>
<a href="https://pkg.go.dev/github.com/84hero/evm-scanner">
<img src="https://pkg.go.dev/badge/github.com/84hero/evm-scanner.svg" alt="Go Reference">
</a>
<a href="https://github.com/84hero/evm-scanner/releases">
<img src="https://img.shields.io/github/v/release/84hero/evm-scanner" alt="Release">
</a>
<a href="https://github.com/84hero/evm-scanner/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/84hero/evm-scanner" alt="License">
</a>
</p>

**[English](README.md)** | **[简体中文](README_CN.md)**

Expand Down
21 changes: 17 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
# EVM Scanner 🚀

[![Go Report Card](https://goreportcard.com/badge/github.com/84hero/evm-scanner)](https://goreportcard.com/report/github.com/84hero/evm-scanner)
[![Build Status](https://github.com/84hero/evm-scanner/workflows/Test%20and%20Lint/badge.svg)](https://github.com/84hero/evm-scanner/actions)
[![GoDoc](https://godoc.org/github.com/84hero/evm-scanner?status.svg)](https://godoc.org/github.com/84hero/evm-scanner)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<p align="center">
<a href="https://github.com/84hero/evm-scanner/actions/workflows/test.yml">
<img src="https://github.com/84hero/evm-scanner/actions/workflows/test.yml/badge.svg" alt="Build Status">
</a>
<a href="https://goreportcard.com/report/github.com/84hero/evm-scanner">
<img src="https://goreportcard.com/badge/github.com/84hero/evm-scanner" alt="Go Report Card">
</a>
<a href="https://pkg.go.dev/github.com/84hero/evm-scanner">
<img src="https://pkg.go.dev/badge/github.com/84hero/evm-scanner.svg" alt="Go Reference">
</a>
<a href="https://github.com/84hero/evm-scanner/releases">
<img src="https://img.shields.io/github/v/release/84hero/evm-scanner" alt="Release">
</a>
<a href="https://github.com/84hero/evm-scanner/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/84hero/evm-scanner" alt="License">
</a>
</p>

**[English](README.md)** | **[简体中文](README_CN.md)**

Expand Down
28 changes: 14 additions & 14 deletions cmd/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,20 @@ func main() {
// 4. Build filter
usdtAddress := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
transferTopic := crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)"))

filter := scanner.NewFilter().
AddContract(usdtAddress).
SetTopic(0, transferTopic)

// 5. Configure Storage [Feature 2: Multiple Storage Engine Support]
var store storage.Persistence

// Storage Prefix (Namespace): Prioritize storage_prefix from config, otherwise use Project name
storePrefix := cfg.Scanner.StoragePrefix
if storePrefix == "" {
storePrefix = cfg.Project + "_"
}

if dbURL := os.Getenv("PG_URL"); dbURL != "" {
// PostgreSQL
pgStore, err := storage.NewPostgresStore(dbURL, storePrefix)
Expand Down Expand Up @@ -113,15 +113,15 @@ func main() {

// 6. Initialize Scanner
scanCfg := scanner.Config{
ChainID: cfg.Scanner.ChainID,
StartBlock: cfg.Scanner.StartBlock,
ForceStart: cfg.Scanner.ForceStart,
Rewind: cfg.Scanner.Rewind,
CursorRewind: cfg.Scanner.CursorRewind,
BatchSize: cfg.Scanner.BatchSize,
Interval: cfg.Scanner.Interval,
ReorgSafe: cfg.Scanner.Confirmations, // Using merged preset values
UseBloom: cfg.Scanner.UseBloom,
ChainID: cfg.Scanner.ChainID,
StartBlock: cfg.Scanner.StartBlock,
ForceStart: cfg.Scanner.ForceStart,
Rewind: cfg.Scanner.Rewind,
CursorRewind: cfg.Scanner.CursorRewind,
BatchSize: cfg.Scanner.BatchSize,
Interval: cfg.Scanner.Interval,
ReorgSafe: cfg.Scanner.Confirmations, // Using merged preset values
UseBloom: cfg.Scanner.UseBloom,
}

s := scanner.New(client, store, scanCfg, filter)
Expand All @@ -137,7 +137,7 @@ func main() {
}

// Print human-readable data
fmt.Printf(" [Event] %s | Block: %d | From: %v | To: %v | Value: %v\n",
fmt.Printf(" [Event] %s | Block: %d | From: %v | To: %v | Value: %v\n",
decoded.Name,
l.BlockNumber,
decoded.Inputs["from"],
Expand All @@ -162,4 +162,4 @@ func main() {
log.Info("Shutting down...")
cancel()
time.Sleep(1 * time.Second)
}
}
28 changes: 14 additions & 14 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,20 @@ func main() {
// 4. Build filter
usdtAddress := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
transferTopic := crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)"))

filter := scanner.NewFilter().
AddContract(usdtAddress).
SetTopic(0, transferTopic)

// 5. Configure Storage [Feature 2: Multiple Storage Engine Support]
var store storage.Persistence

// Storage Prefix (Namespace): Prioritize storage_prefix from config, otherwise use Project name
storePrefix := cfg.Scanner.StoragePrefix
if storePrefix == "" {
storePrefix = cfg.Project + "_"
}

if dbURL := os.Getenv("PG_URL"); dbURL != "" {
// PostgreSQL
pgStore, err := storage.NewPostgresStore(dbURL, storePrefix)
Expand Down Expand Up @@ -113,15 +113,15 @@ func main() {

// 6. Initialize Scanner
scanCfg := scanner.Config{
ChainID: cfg.Scanner.ChainID,
StartBlock: cfg.Scanner.StartBlock,
ForceStart: cfg.Scanner.ForceStart,
Rewind: cfg.Scanner.Rewind,
CursorRewind: cfg.Scanner.CursorRewind,
BatchSize: cfg.Scanner.BatchSize,
Interval: cfg.Scanner.Interval,
ReorgSafe: cfg.Scanner.Confirmations, // Using merged preset values
UseBloom: cfg.Scanner.UseBloom,
ChainID: cfg.Scanner.ChainID,
StartBlock: cfg.Scanner.StartBlock,
ForceStart: cfg.Scanner.ForceStart,
Rewind: cfg.Scanner.Rewind,
CursorRewind: cfg.Scanner.CursorRewind,
BatchSize: cfg.Scanner.BatchSize,
Interval: cfg.Scanner.Interval,
ReorgSafe: cfg.Scanner.Confirmations, // Using merged preset values
UseBloom: cfg.Scanner.UseBloom,
}

s := scanner.New(client, store, scanCfg, filter)
Expand All @@ -137,7 +137,7 @@ func main() {
}

// Print human-readable data
fmt.Printf(" [Event] %s | Block: %d | From: %v | To: %v | Value: %v\n",
fmt.Printf(" [Event] %s | Block: %d | From: %v | To: %v | Value: %v\n",
decoded.Name,
l.BlockNumber,
decoded.Inputs["from"],
Expand All @@ -162,4 +162,4 @@ func main() {
log.Info("Shutting down...")
cancel()
time.Sleep(1 * time.Second)
}
}
14 changes: 7 additions & 7 deletions examples/custom-chain/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
func main() {
// [Scenario] We are launching a new AppChain or L2 called "HeroChain"
// HeroChain has a fast 1-second block time and needs 50 confirmations for safety.

// 1. Register the New Chain Preset
chain.Register("herochain", chain.Preset{
ChainID: "888",
Expand All @@ -35,11 +35,11 @@ func main() {
preset, _ := chain.Get("herochain")

config := scanner.Config{
ChainID: "herochain",
BatchSize: preset.BatchSize, // Use value from preset
ReorgSafe: preset.ReorgSafe, // Use value from preset
Interval: preset.BlockTime, // Sync interval matches block time
UseBloom: true, // Enable bloom filter for performance
ChainID: "herochain",
BatchSize: preset.BatchSize, // Use value from preset
ReorgSafe: preset.ReorgSafe, // Use value from preset
Interval: preset.BlockTime, // Sync interval matches block time
UseBloom: true, // Enable bloom filter for performance
}

filter := scanner.NewFilter() // Scan all logs for demonstration
Expand All @@ -52,6 +52,6 @@ func main() {
})

fmt.Printf("Scanner configured for %s (Safety Window: %d blocks)\n", config.ChainID, config.ReorgSafe)

// s.Start(ctx) // Execution omitted for demo purposes
}
2 changes: 1 addition & 1 deletion examples/custom-sink/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func main() {
for i, l := range logs {
decoded[i] = sink.DecodedLog{Log: l}
}

// Use our custom sink
return mySlackSink.Send(ctx, decoded)
})
Expand Down
14 changes: 7 additions & 7 deletions examples/multi-sink/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ func main() {

// 4. Setup Multiple Sinks (Pipeline)
var outputs []sink.Output

// Console Sink
outputs = append(outputs, sink.NewConsoleOutput())

// File Sink
if fo, err := sink.NewFileOutput("events.jsonl"); err == nil {
outputs = append(outputs, fo)
Expand All @@ -66,11 +66,11 @@ func main() {

// 5. Initialize Scanner
scanCfg := scanner.Config{
ChainID: "ethereum",
Rewind: 10, // Start from 10 blocks ago
Interval: 5 * time.Second,
ReorgSafe: 2,
BatchSize: 10,
ChainID: "ethereum",
Rewind: 10, // Start from 10 blocks ago
Interval: 5 * time.Second,
ReorgSafe: 2,
BatchSize: 10,
}

s := scanner.New(client, store, scanCfg, filter)
Expand Down
9 changes: 5 additions & 4 deletions internal/webhook/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
)

// Config holds configuration for the Webhook client.
type Config struct {
URL string `mapstructure:"url"`
Secret string `mapstructure:"secret"`
Expand Down Expand Up @@ -50,7 +51,7 @@ func NewClient(cfg Config) *Client {
}
}

// Payload defines the data structure sent via webhook
// Payload defines the data structure sent via webhook to consumers.
type Payload struct {
Timestamp int64 `json:"timestamp"`
Logs []types.Log `json:"logs"`
Expand Down Expand Up @@ -92,7 +93,7 @@ func (c *Client) Send(ctx context.Context, logs []types.Log) error {
return ctx.Err()
case <-timer.C:
}

// Exponential backoff
backoff *= 2
if backoff > c.cfg.MaxBackoff {
Expand All @@ -104,7 +105,7 @@ func (c *Client) Send(ctx context.Context, logs []types.Log) error {
if err == nil {
return nil // Success
}

lastErr = err
// For 4xx client errors (e.g., 400 Bad Request), retries usually don't help.
// Simplified logic: retry for network errors and 5xx.
Expand Down Expand Up @@ -143,4 +144,4 @@ func (c *Client) attemptSend(ctx context.Context, body []byte) error {
}

return nil
}
}
4 changes: 2 additions & 2 deletions internal/webhook/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

func TestWebhookSend(t *testing.T) {
secret := "my-secret"

// 1. Create Mock server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate Headers
Expand Down Expand Up @@ -76,7 +76,7 @@ func TestWebhook_Retry(t *testing.T) {
InitialBackoff: 1 * time.Millisecond,
MaxBackoff: 5 * time.Millisecond,
})

logs := []types.Log{{Index: 1}}
err := client.Send(context.Background(), logs)
assert.NoError(t, err)
Expand Down
20 changes: 10 additions & 10 deletions pkg/chain/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ import (

// Preset defines the default behavior parameters for a chain
type Preset struct {
ChainID string
BlockTime time.Duration // Average block time (affects polling interval)
ReorgSafe uint64 // Recommended safety confirmations
BatchSize uint64 // Recommended scan batch size
Endpoint string // (Optional) Default public RPC
ChainID string
BlockTime time.Duration // Average block time (affects polling interval)
ReorgSafe uint64 // Recommended safety confirmations
BatchSize uint64 // Recommended scan batch size
Endpoint string // (Optional) Default public RPC
}

var (
registry = make(map[string]Preset)
mu sync.RWMutex
)

// Register registers a new chain preset. Users can call this in init() to add custom/private chains.
// Register adds a new chain preset to the global registry.
func Register(name string, p Preset) {
mu.Lock()
defer mu.Unlock()
registry[name] = p
}

// Get retrieves a preset configuration
// Get retrieves a preset configuration from the registry by its name.
func Get(name string) (Preset, bool) {
mu.RLock()
defer mu.RUnlock()
Expand All @@ -42,18 +42,18 @@ func init() {
ReorgSafe: 12,
BatchSize: 100,
})

Register("bsc-mainnet", Preset{
ChainID: "56",
BlockTime: 3 * time.Second,
ReorgSafe: 15, // BSC reorgs are relatively frequent
BatchSize: 200,
})

Register("polygon-mainnet", Preset{
ChainID: "137",
BlockTime: 2 * time.Second,
ReorgSafe: 32, // Polygon recommends deeper confirmations
BatchSize: 200,
})
}
}
Loading