mirror of
https://github.com/grassrootseconomics/eth-tracker.git
synced 2026-05-17 02:15:19 +02:00
release: v1.0.0
This commit is contained in:
31
internal/api/api.go
Normal file
31
internal/api/api.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/stats"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func New(statsCollector *stats.Stats) *bunrouter.Router {
|
||||
router := bunrouter.New()
|
||||
|
||||
router.GET("/metrics", metricsHandler())
|
||||
router.GET("/stats", statsHandler(statsCollector))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func metricsHandler() bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ bunrouter.Request) error {
|
||||
metrics.WritePrometheus(w, true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func statsHandler(s *stats.Stats) bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ bunrouter.Request) error {
|
||||
return bunrouter.JSON(w, s.APIStatsResponse())
|
||||
}
|
||||
}
|
||||
107
internal/backfiller/backfiller.go
Normal file
107
internal/backfiller/backfiller.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package backfiller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/db"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/queue"
|
||||
)
|
||||
|
||||
type (
|
||||
BackfillerOpts struct {
|
||||
DB db.DB
|
||||
Logg *slog.Logger
|
||||
Queue *queue.Queue
|
||||
}
|
||||
|
||||
backfiller struct {
|
||||
db db.DB
|
||||
logg *slog.Logger
|
||||
queue *queue.Queue
|
||||
stopCh chan struct{}
|
||||
ticker *time.Ticker
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
verifierInterval = 20 * time.Second
|
||||
epochBlocksCount = 17_280
|
||||
)
|
||||
|
||||
func New(o BackfillerOpts) *backfiller {
|
||||
return &backfiller{
|
||||
db: o.DB,
|
||||
logg: o.Logg,
|
||||
queue: o.Queue,
|
||||
stopCh: make(chan struct{}),
|
||||
ticker: time.NewTicker(verifierInterval),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backfiller) Stop() {
|
||||
b.ticker.Stop()
|
||||
b.stopCh <- struct{}{}
|
||||
}
|
||||
|
||||
func (b *backfiller) Start() {
|
||||
for {
|
||||
select {
|
||||
case <-b.stopCh:
|
||||
b.logg.Info("verifier shutting down")
|
||||
b.ticker.Stop()
|
||||
return
|
||||
case <-b.ticker.C:
|
||||
if b.queue.Size() <= 1 {
|
||||
if err := b.Run(true); err != nil {
|
||||
b.logg.Error("verifier tick run error", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backfiller) Run(skipLatest bool) error {
|
||||
lower, err := b.db.GetLowerBound()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifier could not get lower bound from db: err %v", err)
|
||||
}
|
||||
upper, err := b.db.GetUpperBound()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifier could not get upper bound from db: err %v", err)
|
||||
}
|
||||
|
||||
if skipLatest {
|
||||
upper--
|
||||
}
|
||||
|
||||
missingBlocks, err := b.db.GetMissingValuesBitSet(lower, upper)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifier could not get missing values bitset: err %v", err)
|
||||
}
|
||||
missingBlocksCount := missingBlocks.Count()
|
||||
|
||||
if missingBlocksCount > 0 {
|
||||
if missingBlocksCount >= epochBlocksCount {
|
||||
b.logg.Warn("large number of blocks missing this may result in degraded RPC performance set FORCE_BACKFILL=* to continue", "missing_blocks", missingBlocksCount)
|
||||
_, ok := os.LookupEnv("FORCE_BACKFILL")
|
||||
if !ok {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
b.logg.Info("bootstrapping queue with missing blocks")
|
||||
|
||||
b.logg.Info("found missing blocks", "skip_latest", skipLatest, "missing_blocks_count", missingBlocksCount)
|
||||
buffer := make([]uint, missingBlocksCount)
|
||||
missingBlocks.NextSetMany(0, buffer)
|
||||
defer missingBlocks.ClearAll()
|
||||
|
||||
for _, block := range buffer {
|
||||
b.queue.Push(uint64(block))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
internal/cache/cache.go
vendored
Normal file
61
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/chain"
|
||||
)
|
||||
|
||||
type (
|
||||
Cache interface {
|
||||
Purge() error
|
||||
Exists(string) bool
|
||||
Add(string, bool)
|
||||
Remove(string)
|
||||
IsWatchableIndex(string) bool
|
||||
Size() int
|
||||
}
|
||||
CacheOpts struct {
|
||||
Chain chain.Chain
|
||||
Logg *slog.Logger
|
||||
CacheType string
|
||||
Blacklist []string
|
||||
Registries []string
|
||||
Watchlist []string
|
||||
}
|
||||
)
|
||||
|
||||
func New(o CacheOpts) (Cache, error) {
|
||||
var cache Cache
|
||||
|
||||
switch o.CacheType {
|
||||
case "map":
|
||||
cache = NewMapCache()
|
||||
default:
|
||||
cache = NewMapCache()
|
||||
o.Logg.Warn("invalid cache type, using default type (map)")
|
||||
}
|
||||
|
||||
geSmartContracts, err := o.Chain.Provider().GetGESmartContracts(
|
||||
context.Background(),
|
||||
o.Registries,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache could not bootstrap GE smart contracts: err %v", err)
|
||||
}
|
||||
|
||||
for k, v := range geSmartContracts {
|
||||
cache.Add(k, v)
|
||||
}
|
||||
for _, address := range o.Watchlist {
|
||||
cache.Add(address, false)
|
||||
}
|
||||
for _, address := range o.Blacklist {
|
||||
cache.Remove(address)
|
||||
}
|
||||
o.Logg.Info("cache bootstrap complete", "cached_addresses", cache.Size())
|
||||
|
||||
return cache, nil
|
||||
}
|
||||
48
internal/cache/xmap.go
vendored
Normal file
48
internal/cache/xmap.go
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
type mapCache struct {
|
||||
xmap *xsync.Map
|
||||
}
|
||||
|
||||
func NewMapCache() Cache {
|
||||
return &mapCache{
|
||||
xmap: xsync.NewMap(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mapCache) Purge() error {
|
||||
c.xmap.Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mapCache) Exists(key string) bool {
|
||||
_, ok := c.xmap.Load(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *mapCache) Add(key string, value bool) {
|
||||
c.xmap.Store(key, value)
|
||||
}
|
||||
|
||||
func (c *mapCache) Remove(key string) {
|
||||
c.xmap.Delete(key)
|
||||
}
|
||||
|
||||
func (c *mapCache) Size() int {
|
||||
return c.xmap.Size()
|
||||
}
|
||||
|
||||
func (c *mapCache) IsWatchableIndex(key string) bool {
|
||||
watchable, ok := c.xmap.Load(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
watchableBool, ok := watchable.(bool)
|
||||
if !ok {
|
||||
}
|
||||
return watchableBool
|
||||
}
|
||||
21
internal/chain/chain.go
Normal file
21
internal/chain/chain.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/celo-org/celo-blockchain/core/types"
|
||||
"github.com/grassrootseconomics/celoutils/v3"
|
||||
)
|
||||
|
||||
type Chain interface {
|
||||
GetBlocks(context.Context, []uint64) ([]types.Block, error)
|
||||
GetBlock(context.Context, uint64) (*types.Block, error)
|
||||
GetLatestBlock(context.Context) (uint64, error)
|
||||
GetTransaction(context.Context, common.Hash) (*types.Transaction, error)
|
||||
GetReceipts(context.Context, *types.Block) ([]types.Receipt, error)
|
||||
GetRevertReason(context.Context, common.Hash, *big.Int) (string, error)
|
||||
Provider() *celoutils.Provider
|
||||
IsArchiveNode() bool
|
||||
}
|
||||
139
internal/chain/rpc.go
Normal file
139
internal/chain/rpc.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/celo-org/celo-blockchain/core/types"
|
||||
"github.com/celo-org/celo-blockchain/rpc"
|
||||
"github.com/grassrootseconomics/celoutils/v3"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
"github.com/grassrootseconomics/w3-celo/module/eth"
|
||||
"github.com/grassrootseconomics/w3-celo/w3types"
|
||||
)
|
||||
|
||||
type (
|
||||
RPCOpts struct {
|
||||
RPCEndpoint string
|
||||
ChainID int64
|
||||
IsArchiveNode bool
|
||||
}
|
||||
|
||||
RPC struct {
|
||||
provider *celoutils.Provider
|
||||
isArchiveNode bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewRPCFetcher(o RPCOpts) (Chain, error) {
|
||||
customRPCClient, err := lowTimeoutRPCClient(o.RPCEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chainProvider := celoutils.NewProvider(
|
||||
o.RPCEndpoint,
|
||||
o.ChainID,
|
||||
celoutils.WithClient(customRPCClient),
|
||||
)
|
||||
|
||||
return &RPC{
|
||||
provider: chainProvider,
|
||||
isArchiveNode: o.IsArchiveNode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func lowTimeoutRPCClient(rpcEndpoint string) (*w3.Client, error) {
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.DialHTTPWithClient(
|
||||
rpcEndpoint,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w3.NewClient(rpcClient), nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetBlocks(ctx context.Context, blockNumbers []uint64) ([]types.Block, error) {
|
||||
blocksCount := len(blockNumbers)
|
||||
calls := make([]w3types.RPCCaller, blocksCount)
|
||||
blocks := make([]types.Block, blocksCount)
|
||||
|
||||
for i, v := range blockNumbers {
|
||||
calls[i] = eth.BlockByNumber(new(big.Int).SetUint64(v)).Returns(&blocks[i])
|
||||
}
|
||||
|
||||
if err := c.provider.Client.CallCtx(ctx, calls...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetBlock(ctx context.Context, blockNumber uint64) (*types.Block, error) {
|
||||
var block types.Block
|
||||
blockCall := eth.BlockByNumber(new(big.Int).SetUint64(blockNumber)).Returns(&block)
|
||||
|
||||
if err := c.provider.Client.CallCtx(ctx, blockCall); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &block, nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetLatestBlock(ctx context.Context) (uint64, error) {
|
||||
var latestBlock big.Int
|
||||
latestBlockCall := eth.BlockNumber().Returns(&latestBlock)
|
||||
|
||||
if err := c.provider.Client.CallCtx(ctx, latestBlockCall); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return latestBlock.Uint64(), nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, error) {
|
||||
var transaction types.Transaction
|
||||
if err := c.provider.Client.CallCtx(ctx, eth.Tx(txHash).Returns(&transaction)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &transaction, nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetReceipts(ctx context.Context, block *types.Block) ([]types.Receipt, error) {
|
||||
txCount := len(block.Transactions())
|
||||
|
||||
calls := make([]w3types.RPCCaller, txCount)
|
||||
receipts := make([]types.Receipt, txCount)
|
||||
|
||||
for i, tx := range block.Transactions() {
|
||||
calls[i] = eth.TxReceipt(tx.Hash()).Returns(&receipts[i])
|
||||
}
|
||||
|
||||
if err := c.provider.Client.CallCtx(ctx, calls...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return receipts, nil
|
||||
}
|
||||
|
||||
func (c *RPC) GetRevertReason(ctx context.Context, txHash common.Hash, blockNumber *big.Int) (string, error) {
|
||||
return c.provider.SimulateRevertedTx(ctx, txHash, blockNumber)
|
||||
}
|
||||
|
||||
func (c *RPC) Provider() *celoutils.Provider {
|
||||
return c.provider
|
||||
}
|
||||
|
||||
func (c *RPC) IsArchiveNode() bool {
|
||||
return c.isArchiveNode
|
||||
}
|
||||
182
internal/db/bolt.go
Normal file
182
internal/db/bolt.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type boltDB struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
const (
|
||||
dbFolderName = "tracker_db"
|
||||
|
||||
upperBoundKey = "upper"
|
||||
lowerBoundKey = "lower"
|
||||
)
|
||||
|
||||
var sortableOrder = binary.BigEndian
|
||||
|
||||
func NewBoltDB() (DB, error) {
|
||||
db, err := bolt.Open(dbFolderName, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte("blocks"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket: %s", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return &boltDB{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *boltDB) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
func (d *boltDB) get(k string) ([]byte, error) {
|
||||
var v []byte
|
||||
err := d.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("blocks"))
|
||||
v = b.Get([]byte(k))
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (d *boltDB) setUint64(k string, v uint64) error {
|
||||
err := d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("blocks"))
|
||||
return b.Put([]byte(k), marshalUint64(v))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *boltDB) setUint64AsKey(v uint64) error {
|
||||
err := d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("blocks"))
|
||||
return b.Put(marshalUint64(v), nil)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalUint64(b []byte) uint64 {
|
||||
return sortableOrder.Uint64(b)
|
||||
}
|
||||
|
||||
func marshalUint64(v uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
sortableOrder.PutUint64(b, v)
|
||||
return b
|
||||
}
|
||||
|
||||
func (d *boltDB) SetLowerBound(v uint64) error {
|
||||
return d.setUint64(lowerBoundKey, v)
|
||||
}
|
||||
|
||||
func (d *boltDB) GetLowerBound() (uint64, error) {
|
||||
v, err := d.get(lowerBoundKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return unmarshalUint64(v), nil
|
||||
}
|
||||
|
||||
func (d *boltDB) SetUpperBound(v uint64) error {
|
||||
return d.setUint64(upperBoundKey, v)
|
||||
}
|
||||
|
||||
func (d *boltDB) GetUpperBound() (uint64, error) {
|
||||
v, err := d.get(upperBoundKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return unmarshalUint64(v), nil
|
||||
}
|
||||
|
||||
func (d *boltDB) SetValue(v uint64) error {
|
||||
return d.setUint64AsKey(v)
|
||||
}
|
||||
|
||||
func (d *boltDB) GetMissingValuesBitSet(lowerBound uint64, upperBound uint64) (*bitset.BitSet, error) {
|
||||
var (
|
||||
b bitset.BitSet
|
||||
)
|
||||
|
||||
err := d.db.View(func(tx *bolt.Tx) error {
|
||||
var (
|
||||
lowerRaw = marshalUint64(lowerBound)
|
||||
upperRaw = marshalUint64(upperBound)
|
||||
)
|
||||
|
||||
for i := lowerBound; i <= upperBound; i++ {
|
||||
b.Set(uint(i))
|
||||
}
|
||||
|
||||
c := tx.Bucket([]byte("blocks")).Cursor()
|
||||
|
||||
for k, _ := c.Seek(lowerRaw); k != nil && bytes.Compare(k, upperRaw) <= 0; k, _ = c.Next() {
|
||||
b.Clear(uint(unmarshalUint64(k)))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (d *boltDB) Cleanup() error {
|
||||
lowerBound, err := d.GetLowerBound()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := marshalUint64(lowerBound - 1)
|
||||
|
||||
err = d.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("blocks"))
|
||||
c := b.Cursor()
|
||||
|
||||
for k, _ := c.First(); k != nil && bytes.Compare(k, target) <= 0; k, _ = c.Next() {
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
48
internal/db/db.go
Normal file
48
internal/db/db.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
type (
|
||||
DB interface {
|
||||
Close() error
|
||||
GetLowerBound() (uint64, error)
|
||||
SetLowerBound(v uint64) error
|
||||
SetUpperBound(uint64) error
|
||||
GetUpperBound() (uint64, error)
|
||||
SetValue(uint64) error
|
||||
GetMissingValuesBitSet(uint64, uint64) (*bitset.BitSet, error)
|
||||
Cleanup() error
|
||||
}
|
||||
|
||||
DBOpts struct {
|
||||
Logg *slog.Logger
|
||||
DBType string
|
||||
}
|
||||
)
|
||||
|
||||
func New(o DBOpts) (DB, error) {
|
||||
var (
|
||||
err error
|
||||
db DB
|
||||
)
|
||||
|
||||
switch o.DBType {
|
||||
case "bolt":
|
||||
db, err = NewBoltDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
db, err = NewBoltDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Logg.Warn("invalid db type, using default type (bolt)")
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
37
internal/event/event.go
Normal file
37
internal/event/event.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package event
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type (
|
||||
Event struct {
|
||||
Block uint64 `json:"block"`
|
||||
ContractAddress string `json:"contractAddress"`
|
||||
Success bool `json:"success"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
TxHash string `json:"transactionHash"`
|
||||
TxType string `json:"transactionType"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
Index uint `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func (e Event) Serialize() ([]byte, error) {
|
||||
jsonData, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonData, err
|
||||
}
|
||||
|
||||
func Deserialize(jsonData []byte) (Event, error) {
|
||||
var (
|
||||
event Event
|
||||
)
|
||||
|
||||
if err := json.Unmarshal(jsonData, &event); err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
112
internal/handler/faucet_give.go
Normal file
112
internal/handler/faucet_give.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/celoutils/v3"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type faucetGiveHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const faucetGiveEventName = "FAUCET_GIVE"
|
||||
|
||||
var (
|
||||
faucetGiveTopicHash = w3.H("0x26162814817e23ec5035d6a2edc6c422da2da2119e27cfca6be65cc2dc55ca4c")
|
||||
faucetGiveEvent = w3.MustNewEvent("Give(address indexed _recipient, address indexed _token, uint256 _amount)")
|
||||
faucetGiveToSig = w3.MustNewFunc("giveTo(address)", "uint256")
|
||||
faucetGimmeSig = w3.MustNewFunc("gimme()", "uint256")
|
||||
)
|
||||
|
||||
func NewFaucetGiveHandler(pub pub.Pub) *faucetGiveHandler {
|
||||
return &faucetGiveHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *faucetGiveHandler) Name() string {
|
||||
return faucetGiveEventName
|
||||
}
|
||||
|
||||
func (h *faucetGiveHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == faucetGiveTopicHash {
|
||||
var (
|
||||
recipient common.Address
|
||||
token common.Address
|
||||
amount big.Int
|
||||
)
|
||||
|
||||
if err := faucetGiveEvent.DecodeArgs(msg.Log, &recipient, &token, &amount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
faucetGiveEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: faucetGiveEventName,
|
||||
Payload: map[string]any{
|
||||
"recipient": recipient.Hex(),
|
||||
"token": token.Hex(),
|
||||
"amount": amount.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, faucetGiveEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *faucetGiveHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
faucetGiveEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: faucetGiveEventName,
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "63e4bff4":
|
||||
var to common.Address
|
||||
|
||||
if err := faucetGiveToSig.DecodeArgs(w3.B(msg.InputData), &to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
faucetGiveEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"recipient": to.Hex(),
|
||||
"token": celoutils.ZeroAddress,
|
||||
"amount": "0",
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, faucetGiveEvent)
|
||||
case "de82efb4":
|
||||
faucetGiveEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"recipient": celoutils.ZeroAddress,
|
||||
"token": celoutils.ZeroAddress,
|
||||
"amount": "0",
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, faucetGiveEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
internal/handler/handler.go
Normal file
50
internal/handler/handler.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/core/types"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/cache"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
)
|
||||
|
||||
type (
|
||||
Handler interface {
|
||||
Name() string
|
||||
HandleLog(context.Context, LogMessage) error
|
||||
HandleRevert(context.Context, RevertMessage) error
|
||||
}
|
||||
|
||||
HandlerPipeline []Handler
|
||||
|
||||
LogMessage struct {
|
||||
Log *types.Log
|
||||
Timestamp uint64
|
||||
}
|
||||
|
||||
RevertMessage struct {
|
||||
From string
|
||||
RevertReason string
|
||||
InputData string
|
||||
Block uint64
|
||||
ContractAddress string
|
||||
Timestamp uint64
|
||||
TxHash string
|
||||
}
|
||||
)
|
||||
|
||||
func New(pub pub.Pub, cache cache.Cache) HandlerPipeline {
|
||||
return []Handler{
|
||||
NewTokenTransferHandler(pub),
|
||||
NewPoolSwapHandler(pub),
|
||||
NewFaucetGiveHandler(pub),
|
||||
NewPoolDepositHandler(pub),
|
||||
NewTokenMintHandler(pub),
|
||||
NewTokenBurnHandler(pub),
|
||||
NewQuoterPriceHandler(pub),
|
||||
NewOwnershipHandler(pub),
|
||||
NewSealHandler(pub),
|
||||
NewIndexAddHandler(pub, cache),
|
||||
NewIndexRemoveHandler(pub, cache),
|
||||
}
|
||||
}
|
||||
113
internal/handler/index_add.go
Normal file
113
internal/handler/index_add.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/cache"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type indexAddHandler struct {
|
||||
pub pub.Pub
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
const indexAddEventName = "INDEX_ADD"
|
||||
|
||||
var (
|
||||
indexAddTopicHash = w3.H("0xa226db3f664042183ee0281230bba26cbf7b5057e50aee7f25a175ff45ce4d7f")
|
||||
indexAddEvent = w3.MustNewEvent("AddressAdded(address _token)")
|
||||
indexAddSig = w3.MustNewFunc("add(address)", "bool")
|
||||
indexRegisterSig = w3.MustNewFunc("register(address)", "bool")
|
||||
)
|
||||
|
||||
func NewIndexAddHandler(pub pub.Pub, cache cache.Cache) *indexAddHandler {
|
||||
return &indexAddHandler{
|
||||
pub: pub,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *indexAddHandler) Name() string {
|
||||
return indexAddEventName
|
||||
}
|
||||
|
||||
func (h *indexAddHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == indexAddTopicHash {
|
||||
var address common.Address
|
||||
|
||||
if err := indexAddEvent.DecodeArgs(msg.Log, &address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexAddEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: indexAddEventName,
|
||||
Payload: map[string]any{
|
||||
"address": address.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
if h.cache.IsWatchableIndex(address.Hex()) {
|
||||
h.cache.Add(address.Hex(), false)
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, indexAddEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *indexAddHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
indexAddEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: indexAddEventName,
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "0a3b0a4f":
|
||||
var address common.Address
|
||||
|
||||
indexAddEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"address": address.Hex(),
|
||||
}
|
||||
|
||||
if err := indexAddSig.DecodeArgs(w3.B(msg.InputData), &address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, indexAddEvent)
|
||||
case "4420e486":
|
||||
var address common.Address
|
||||
|
||||
indexAddEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"address": address.Hex(),
|
||||
}
|
||||
|
||||
if err := indexRegisterSig.DecodeArgs(w3.B(msg.InputData), &address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, indexAddEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
98
internal/handler/index_remove.go
Normal file
98
internal/handler/index_remove.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/cache"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type indexRemoveHandler struct {
|
||||
pub pub.Pub
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
const indexRemoveEventName = "INDEX_REMOVE"
|
||||
|
||||
var (
|
||||
indexRemoveTopicHash = w3.H("0x24a12366c02e13fe4a9e03d86a8952e85bb74a456c16e4a18b6d8295700b74bb")
|
||||
indexRemoveEvent = w3.MustNewEvent("AddressRemoved(address _token)")
|
||||
indexRemoveSig = w3.MustNewFunc("remove(address)", "bool")
|
||||
)
|
||||
|
||||
func NewIndexRemoveHandler(pub pub.Pub, cache cache.Cache) *indexRemoveHandler {
|
||||
return &indexRemoveHandler{
|
||||
pub: pub,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *indexRemoveHandler) Name() string {
|
||||
return indexRemoveEventName
|
||||
}
|
||||
|
||||
func (h *indexRemoveHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == indexRemoveTopicHash {
|
||||
var address common.Address
|
||||
|
||||
if err := indexRemoveEvent.DecodeArgs(msg.Log, &address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexRemoveEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: indexRemoveEventName,
|
||||
Payload: map[string]any{
|
||||
"address": address.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
if h.cache.IsWatchableIndex(address.Hex()) {
|
||||
h.cache.Remove(address.Hex())
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, indexRemoveEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *indexRemoveHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "29092d0e":
|
||||
var address common.Address
|
||||
|
||||
if err := indexRemoveSig.DecodeArgs(w3.B(msg.InputData), &address); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexRemoveEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: indexRemoveEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"address": address.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, indexRemoveEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
98
internal/handler/ownership.go
Normal file
98
internal/handler/ownership.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type ownershipHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const (
|
||||
ownershipEventName = "OWNERSHIP_TRANSFERRED"
|
||||
)
|
||||
|
||||
var (
|
||||
ownershipTopicHash = w3.H("0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0")
|
||||
ownershipEvent = w3.MustNewEvent("OwnershipTransferred(address indexed previousOwner, address indexed newOwner)")
|
||||
ownershipToSig = w3.MustNewFunc("transferOwnership(address)", "bool")
|
||||
)
|
||||
|
||||
func NewOwnershipHandler(pub pub.Pub) *ownershipHandler {
|
||||
return &ownershipHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ownershipHandler) Name() string {
|
||||
return ownershipEventName
|
||||
}
|
||||
|
||||
func (h *ownershipHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == ownershipTopicHash {
|
||||
var (
|
||||
previousOwner common.Address
|
||||
newOwner common.Address
|
||||
)
|
||||
|
||||
if err := ownershipEvent.DecodeArgs(msg.Log, &previousOwner, &newOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ownershipEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: ownershipEventName,
|
||||
Payload: map[string]any{
|
||||
"previousOwner": previousOwner.Hex(),
|
||||
"newOwner": newOwner.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, ownershipEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ownershipHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "f2fde38b":
|
||||
var newOwner common.Address
|
||||
|
||||
if err := ownershipToSig.DecodeArgs(w3.B(msg.InputData), &newOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ownershipEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: ownershipEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"previousOwner": msg.From,
|
||||
"newOwner": newOwner.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, ownershipEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
108
internal/handler/pool_deposit.go
Normal file
108
internal/handler/pool_deposit.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type poolDepositHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const poolDepositEventName = "POOL_DEPOSIT"
|
||||
|
||||
var (
|
||||
poolDepositTopicHash = w3.H("0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62")
|
||||
poolDepositEvent = w3.MustNewEvent("Deposit(address indexed initiator, address indexed tokenIn, uint256 amountIn)")
|
||||
poolDepositSig = w3.MustNewFunc("deposit(address, uint256)", "")
|
||||
)
|
||||
|
||||
func NewPoolDepositHandler(pub pub.Pub) *poolDepositHandler {
|
||||
return &poolDepositHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *poolDepositHandler) Name() string {
|
||||
return poolDepositEventName
|
||||
}
|
||||
|
||||
func (h *poolDepositHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == poolDepositTopicHash {
|
||||
var (
|
||||
initiator common.Address
|
||||
tokenIn common.Address
|
||||
amountIn big.Int
|
||||
)
|
||||
|
||||
if err := poolDepositEvent.DecodeArgs(
|
||||
msg.Log,
|
||||
&initiator,
|
||||
&tokenIn,
|
||||
&amountIn,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolDepositEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: poolDepositEventName,
|
||||
Payload: map[string]any{
|
||||
"initiator": initiator.Hex(),
|
||||
"tokenIn": tokenIn.Hex(),
|
||||
"amountIn": amountIn.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, poolDepositEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *poolDepositHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "47e7ef24":
|
||||
var (
|
||||
tokenIn common.Address
|
||||
amountIn big.Int
|
||||
)
|
||||
|
||||
if err := poolDepositSig.DecodeArgs(w3.B(msg.InputData), &tokenIn, &amountIn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolDepositEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: poolDepositEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"initiator": msg.From,
|
||||
"tokenIn": tokenIn.Hex(),
|
||||
"amountIn": amountIn.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, poolDepositEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
121
internal/handler/pool_swap.go
Normal file
121
internal/handler/pool_swap.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type poolSwapHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const poolSwapEventName = "POOL_SWAP"
|
||||
|
||||
var (
|
||||
poolSwapTopicHash = w3.H("0xd6d34547c69c5ee3d2667625c188acf1006abb93e0ee7cf03925c67cf7760413")
|
||||
poolSwapEvent = w3.MustNewEvent("Swap(address indexed initiator, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 fee)")
|
||||
poolSwapSig = w3.MustNewFunc("withdraw(address, address, uint256)", "")
|
||||
)
|
||||
|
||||
func NewPoolSwapHandler(pub pub.Pub) *poolSwapHandler {
|
||||
return &poolSwapHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *poolSwapHandler) Name() string {
|
||||
return poolSwapEventName
|
||||
}
|
||||
|
||||
func (h *poolSwapHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == poolSwapTopicHash {
|
||||
var (
|
||||
initiator common.Address
|
||||
tokenIn common.Address
|
||||
tokenOut common.Address
|
||||
amountIn big.Int
|
||||
amountOut big.Int
|
||||
fee big.Int
|
||||
)
|
||||
|
||||
if err := poolSwapEvent.DecodeArgs(
|
||||
msg.Log,
|
||||
&initiator,
|
||||
&tokenIn,
|
||||
&tokenOut,
|
||||
&amountIn,
|
||||
&amountOut,
|
||||
&fee,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolSwapEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: poolSwapEventName,
|
||||
Payload: map[string]any{
|
||||
"initiator": initiator.Hex(),
|
||||
"tokenIn": tokenIn.Hex(),
|
||||
"tokenOut": tokenOut.Hex(),
|
||||
"amountIn": amountIn.String(),
|
||||
"amountOut": amountOut.String(),
|
||||
"fee": fee.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, poolSwapEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *poolSwapHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "d9caed12":
|
||||
var (
|
||||
tokenOut common.Address
|
||||
tokenIn common.Address
|
||||
amountIn big.Int
|
||||
)
|
||||
|
||||
if err := poolSwapSig.DecodeArgs(w3.B(msg.InputData), &tokenOut, &tokenIn, &amountIn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poolSwapEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: poolSwapEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"initiator": msg.From,
|
||||
"tokenIn": tokenIn.Hex(),
|
||||
"tokenOut": tokenOut.Hex(),
|
||||
"amountIn": amountIn.String(),
|
||||
"amountOut": "0",
|
||||
"fee": "0",
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, poolSwapEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
100
internal/handler/quoter_price.go
Normal file
100
internal/handler/quoter_price.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type quoterPriceHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const quoterPriceEventName = "QUOTER_PRICE_INDEX_UPDATED"
|
||||
|
||||
var (
|
||||
quoterPriceTopicHash = w3.H("0xdb9ce1a76955721ca61ac50cd1b87f9ab8620325c8619a62192c2dc7871d56b1")
|
||||
quoterPriceEvent = w3.MustNewEvent("PriceIndexUpdated(address _tokenAddress, uint256 _exchangeRate)")
|
||||
quoterPriceToSig = w3.MustNewFunc("setPriceIndexValue(address, uint256)", "uint256")
|
||||
)
|
||||
|
||||
func NewQuoterPriceHandler(pub pub.Pub) *quoterPriceHandler {
|
||||
return "erPriceHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *quoterPriceHandler) Name() string {
|
||||
return quoterPriceEventName
|
||||
}
|
||||
|
||||
func (h *quoterPriceHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == quoterPriceTopicHash {
|
||||
var (
|
||||
token common.Address
|
||||
exchangeRate big.Int
|
||||
)
|
||||
|
||||
if err := quoterPriceEvent.DecodeArgs(msg.Log, &token, &exchangeRate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quoterPriceEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: quoterPriceEventName,
|
||||
Payload: map[string]any{
|
||||
"token": token.Hex(),
|
||||
"exchangeRate": exchangeRate.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, quoterPriceEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *quoterPriceHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "ebc59dff":
|
||||
var (
|
||||
token common.Address
|
||||
exchangeRate big.Int
|
||||
)
|
||||
|
||||
if err := quoterPriceToSig.DecodeArgs(w3.B(msg.InputData), &token, &exchangeRate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quoterPriceEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: quoterPriceEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"token": token.Hex(),
|
||||
"exchangeRate": exchangeRate.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, quoterPriceEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
95
internal/handler/seal.go
Normal file
95
internal/handler/seal.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type sealHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const sealEventName = "SEAL_STATE_CHANGE"
|
||||
|
||||
var (
|
||||
sealTopicHash = w3.H("0x6b7e2e653f93b645d4ed7292d6429f96637084363e477c8aaea1a43ed13c284e")
|
||||
sealEvent = w3.MustNewEvent("SealStateChange(bool indexed _final, uint256 _sealState)")
|
||||
sealToSig = w3.MustNewFunc("seal(uint256)", "uint256")
|
||||
)
|
||||
|
||||
func NewSealHandler(pub pub.Pub) *sealHandler {
|
||||
return &sealHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *sealHandler) Name() string {
|
||||
return sealEventName
|
||||
}
|
||||
|
||||
func (h *sealHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == sealTopicHash {
|
||||
var (
|
||||
final bool
|
||||
sealState big.Int
|
||||
)
|
||||
|
||||
if err := sealEvent.DecodeArgs(msg.Log, &final, &sealState); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sealEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: sealEventName,
|
||||
Payload: map[string]any{
|
||||
"final": final,
|
||||
"sealState": sealState.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, sealEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *sealHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "86fe212d":
|
||||
var sealState big.Int
|
||||
|
||||
if err := sealToSig.DecodeArgs(w3.B(msg.InputData), &sealState); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sealEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: sealEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"sealState": sealState.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, sealEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
97
internal/handler/token_burn.go
Normal file
97
internal/handler/token_burn.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type tokenBurnHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const burnEventName = "TOKEN_BURN"
|
||||
|
||||
var (
|
||||
tokenBurnTopicHash = w3.H("0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5")
|
||||
tokenBurnEvent = w3.MustNewEvent("tokenBurn(address indexed _tokenBurner, uint256 _value)")
|
||||
tokenBurnToSig = w3.MustNewFunc("tokenBurn(uint256)", "bool")
|
||||
)
|
||||
|
||||
func NewTokenBurnHandler(pub pub.Pub) *tokenBurnHandler {
|
||||
return &tokenBurnHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *tokenBurnHandler) Name() string {
|
||||
return burnEventName
|
||||
}
|
||||
|
||||
func (h *tokenBurnHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == tokenBurnTopicHash {
|
||||
var (
|
||||
tokenBurner common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenBurnEvent.DecodeArgs(msg.Log, &tokenBurner, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenBurnEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: burnEventName,
|
||||
Payload: map[string]any{
|
||||
"tokenBurner": tokenBurner.Hex(),
|
||||
"value": value.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenBurnEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *tokenBurnHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "42966c68":
|
||||
var value big.Int
|
||||
|
||||
if err := tokenBurnToSig.DecodeArgs(w3.B(msg.InputData), &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenBurnEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: burnEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"tokenBurner": msg.From,
|
||||
"value": value.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenBurnEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
103
internal/handler/token_mint.go
Normal file
103
internal/handler/token_mint.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type tokenMintHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const mintEventName = "TOKEN_MINT"
|
||||
|
||||
var (
|
||||
tokenMintTopicHash = w3.H("0xab8530f87dc9b59234c4623bf917212bb2536d647574c8e7e5da92c2ede0c9f8")
|
||||
tokenMintEvent = w3.MustNewEvent("Mint(address indexed _tokenMinter, address indexed _beneficiary, uint256 _value)")
|
||||
tokenMintToSig = w3.MustNewFunc("MintTo(address, uint256)", "bool")
|
||||
)
|
||||
|
||||
func NewTokenMintHandler(pub pub.Pub) *tokenMintHandler {
|
||||
return &tokenMintHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *tokenMintHandler) Name() string {
|
||||
return mintEventName
|
||||
}
|
||||
|
||||
func (h *tokenMintHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == tokenMintTopicHash {
|
||||
var (
|
||||
tokenMinter common.Address
|
||||
to common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenMintEvent.DecodeArgs(msg.Log, &tokenMinter, &to, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenMintEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: mintEventName,
|
||||
Payload: map[string]any{
|
||||
"tokenMinter": tokenMinter.Hex(),
|
||||
"to": to.Hex(),
|
||||
"value": value.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenMintEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *tokenMintHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "449a52f8":
|
||||
var (
|
||||
to common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenMintToSig.DecodeArgs(w3.B(msg.InputData), &to, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenMintEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: mintEventName,
|
||||
Payload: map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"tokenMinter": msg.From,
|
||||
"to": to.Hex(),
|
||||
"value": value.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenMintEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
124
internal/handler/token_transfer.go
Normal file
124
internal/handler/token_transfer.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/w3-celo"
|
||||
)
|
||||
|
||||
type tokenTransferHandler struct {
|
||||
pub pub.Pub
|
||||
}
|
||||
|
||||
const transferEventName = "TOKEN_TRANSFER"
|
||||
|
||||
var (
|
||||
tokenTransferTopicHash = w3.H("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||
tokenTransferEvent = w3.MustNewEvent("Transfer(address indexed _from, address indexed _to, uint256 _value)")
|
||||
tokenTransferSig = w3.MustNewFunc("transfer(address, uint256)", "bool")
|
||||
tokenTransferFromSig = w3.MustNewFunc("transferFrom(address, address, uint256)", "bool")
|
||||
)
|
||||
|
||||
func NewTokenTransferHandler(pub pub.Pub) *tokenTransferHandler {
|
||||
return &tokenTransferHandler{
|
||||
pub: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *tokenTransferHandler) Name() string {
|
||||
return transferEventName
|
||||
}
|
||||
|
||||
func (h *tokenTransferHandler) HandleLog(ctx context.Context, msg LogMessage) error {
|
||||
if msg.Log.Topics[0] == tokenTransferTopicHash {
|
||||
var (
|
||||
from common.Address
|
||||
to common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenTransferEvent.DecodeArgs(msg.Log, &from, &to, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenTransferEvent := event.Event{
|
||||
Index: msg.Log.Index,
|
||||
Block: msg.Log.BlockNumber,
|
||||
ContractAddress: msg.Log.Address.Hex(),
|
||||
Success: true,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.Log.TxHash.Hex(),
|
||||
TxType: transferEventName,
|
||||
Payload: map[string]any{
|
||||
"from": from.Hex(),
|
||||
"to": to.Hex(),
|
||||
"value": value.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenTransferEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *tokenTransferHandler) HandleRevert(ctx context.Context, msg RevertMessage) error {
|
||||
if len(msg.InputData) < 8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tokenTransferEvent := event.Event{
|
||||
Block: msg.Block,
|
||||
ContractAddress: msg.ContractAddress,
|
||||
Success: false,
|
||||
Timestamp: msg.Timestamp,
|
||||
TxHash: msg.TxHash,
|
||||
TxType: transferEventName,
|
||||
}
|
||||
|
||||
switch msg.InputData[:8] {
|
||||
case "a9059cbb":
|
||||
var (
|
||||
to common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenTransferSig.DecodeArgs(w3.B(msg.InputData), &to, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenTransferEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"from": msg.From,
|
||||
"to": to.Hex(),
|
||||
"value": value.String(),
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenTransferEvent)
|
||||
case "23b872dd":
|
||||
var (
|
||||
from common.Address
|
||||
to common.Address
|
||||
value big.Int
|
||||
)
|
||||
|
||||
if err := tokenTransferFromSig.DecodeArgs(w3.B(msg.InputData), &from, &to, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenTransferEvent.Payload = map[string]any{
|
||||
"revertReason": msg.RevertReason,
|
||||
"from": from.Hex(),
|
||||
"to": to.Hex(),
|
||||
"value": value.String(),
|
||||
}
|
||||
|
||||
return h.pub.Send(ctx, tokenTransferEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
28
internal/pool/pool.go
Normal file
28
internal/pool/pool.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
)
|
||||
|
||||
type PoolOpts struct {
|
||||
Logg *slog.Logger
|
||||
WorkerCount int
|
||||
}
|
||||
|
||||
func NewPool(o PoolOpts) *pond.WorkerPool {
|
||||
return pond.New(
|
||||
o.WorkerCount,
|
||||
1,
|
||||
pond.Strategy(pond.Balanced()),
|
||||
pond.PanicHandler(panicHandler(o.Logg)),
|
||||
)
|
||||
}
|
||||
|
||||
func panicHandler(logg *slog.Logger) func(interface{}) {
|
||||
return func(panic interface{}) {
|
||||
logg.Error("block processor goroutine exited from a panic", "error", panic, "stack_trace", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
147
internal/processor/processor.go
Normal file
147
internal/processor/processor.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/celo-org/celo-blockchain/core/types"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/cache"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/chain"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/db"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/handler"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/pub"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/stats"
|
||||
)
|
||||
|
||||
type (
|
||||
ProcessorOpts struct {
|
||||
Cache cache.Cache
|
||||
DB db.DB
|
||||
Chain chain.Chain
|
||||
Pub pub.Pub
|
||||
Logg *slog.Logger
|
||||
Stats *stats.Stats
|
||||
}
|
||||
|
||||
Processor struct {
|
||||
cache cache.Cache
|
||||
db db.DB
|
||||
chain chain.Chain
|
||||
handlerPipeline handler.HandlerPipeline
|
||||
logg *slog.Logger
|
||||
stats *stats.Stats
|
||||
}
|
||||
)
|
||||
|
||||
func NewProcessor(o ProcessorOpts) *Processor {
|
||||
return &Processor{
|
||||
cache: o.Cache,
|
||||
db: o.DB,
|
||||
handlerPipeline: handler.New(o.Pub, o.Cache),
|
||||
chain: o.Chain,
|
||||
logg: o.Logg,
|
||||
stats: o.Stats,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) ProcessBlock(ctx context.Context, blockNumber uint64) error {
|
||||
block, err := p.chain.GetBlock(ctx, blockNumber)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("block %d error: %v", blockNumber, err)
|
||||
}
|
||||
|
||||
receiptsResp, err := p.chain.GetReceipts(ctx, block)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("receipts fetch error: block %d: %v", blockNumber, err)
|
||||
}
|
||||
|
||||
for _, receipt := range receiptsResp {
|
||||
if receipt.Status > 0 {
|
||||
for _, log := range receipt.Logs {
|
||||
if p.cache.Exists(log.Address.Hex()) {
|
||||
msg := handler.LogMessage{
|
||||
Log: log,
|
||||
Timestamp: block.Time(),
|
||||
}
|
||||
|
||||
if err := p.handleLog(ctx, msg); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("handle logs error: block %d: %v", blockNumber, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if p.isTrieAvailable(blockNumber) {
|
||||
tx, err := p.chain.GetTransaction(ctx, receipt.TxHash)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("get transaction error: tx %s: %v", receipt.TxHash.Hex(), err)
|
||||
}
|
||||
|
||||
if tx.To() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.cache.Exists(tx.To().Hex()) {
|
||||
from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transaction decode error: tx %s: %v", receipt.TxHash.Hex(), err)
|
||||
}
|
||||
|
||||
revertReason, err := p.chain.GetRevertReason(ctx, receipt.TxHash, receipt.BlockNumber)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("get revert reason error: tx %s: %v", receipt.TxHash.Hex(), err)
|
||||
}
|
||||
|
||||
msg := handler.RevertMessage{
|
||||
From: from.Hex(),
|
||||
RevertReason: revertReason,
|
||||
InputData: common.Bytes2Hex(tx.Data()),
|
||||
Block: blockNumber,
|
||||
ContractAddress: tx.To().Hex(),
|
||||
Timestamp: block.Time(),
|
||||
TxHash: receipt.TxHash.Hex(),
|
||||
}
|
||||
|
||||
if err := p.handleRevert(ctx, msg); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("handle revert error: tx %s: %v", receipt.TxHash.Hex(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.db.SetValue(blockNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
p.logg.Debug("successfully processed block", "block", blockNumber)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) isTrieAvailable(blockNumber uint64) bool {
|
||||
available := p.chain.IsArchiveNode() || p.stats.GetLatestBlock()-blockNumber <= 256
|
||||
if !available {
|
||||
p.logg.Warn("skipping block due to potentially missing trie", "block_number", blockNumber)
|
||||
}
|
||||
return available
|
||||
}
|
||||
|
||||
func (p *Processor) handleLog(ctx context.Context, msg handler.LogMessage) error {
|
||||
for _, handler := range p.handlerPipeline {
|
||||
if err := handler.HandleLog(ctx, msg); err != nil {
|
||||
return fmt.Errorf("log handler: %s err: %v", handler.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) handleRevert(ctx context.Context, msg handler.RevertMessage) error {
|
||||
for _, handler := range p.handlerPipeline {
|
||||
if err := handler.HandleRevert(ctx, msg); err != nil {
|
||||
return fmt.Errorf("revert handler: %s err: %v", handler.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
30
internal/pub/console.go
Normal file
30
internal/pub/console.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
)
|
||||
|
||||
type consolePub struct {
|
||||
logg *slog.Logger
|
||||
}
|
||||
|
||||
func NewConsolePub(logg *slog.Logger) Pub {
|
||||
return &consolePub{
|
||||
logg: logg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *consolePub) Send(_ context.Context, payload event.Event) error {
|
||||
data, err := payload.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.logg.Info("emitted event", "json_payload", string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *consolePub) Close() {}
|
||||
92
internal/pub/jetstream.go
Normal file
92
internal/pub/jetstream.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type (
|
||||
JetStreamOpts struct {
|
||||
Logg *slog.Logger
|
||||
Endpoint string
|
||||
DedupDuration time.Duration
|
||||
PersistDuration time.Duration
|
||||
}
|
||||
|
||||
jetStreamPub struct {
|
||||
natsConn *nats.Conn
|
||||
jsCtx nats.JetStreamContext
|
||||
}
|
||||
)
|
||||
|
||||
const streamName string = "TRACKER"
|
||||
|
||||
var streamSubjects = []string{
|
||||
"TRACKER.*",
|
||||
}
|
||||
|
||||
func NewJetStreamPub(o JetStreamOpts) (Pub, error) {
|
||||
natsConn, err := nats.Connect(o.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
js, err := natsConn.JetStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Logg.Info("successfully connected to NATS server")
|
||||
|
||||
stream, err := js.StreamInfo(streamName)
|
||||
if err != nil && !errors.Is(err, nats.ErrStreamNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if stream == nil {
|
||||
_, err := js.AddStream(&nats.StreamConfig{
|
||||
Name: streamName,
|
||||
MaxAge: o.PersistDuration,
|
||||
Storage: nats.FileStorage,
|
||||
Subjects: streamSubjects,
|
||||
Duplicates: o.DedupDuration,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Logg.Info("successfully created NATS JetStream stream", "stream_name", streamName)
|
||||
}
|
||||
|
||||
return &jetStreamPub{
|
||||
natsConn: natsConn,
|
||||
jsCtx: js,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *jetStreamPub) Close() {
|
||||
if p.natsConn != nil {
|
||||
p.natsConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jetStreamPub) Send(_ context.Context, payload event.Event) error {
|
||||
data, err := payload.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = p.jsCtx.Publish(
|
||||
fmt.Sprintf("%s.%s", streamName, payload.TxType),
|
||||
data,
|
||||
nats.MsgId(fmt.Sprintf("%s:%d", payload.TxHash, payload.Index)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
12
internal/pub/pub.go
Normal file
12
internal/pub/pub.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/event"
|
||||
)
|
||||
|
||||
type Pub interface {
|
||||
Send(context.Context, event.Event) error
|
||||
Close()
|
||||
}
|
||||
67
internal/queue/queue.go
Normal file
67
internal/queue/queue.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/processor"
|
||||
)
|
||||
|
||||
type (
|
||||
QueueOpts struct {
|
||||
Logg *slog.Logger
|
||||
Processor *processor.Processor
|
||||
Pool *pond.WorkerPool
|
||||
}
|
||||
|
||||
Queue struct {
|
||||
logg *slog.Logger
|
||||
processChan chan uint64
|
||||
stopSignal chan interface{}
|
||||
processor *processor.Processor
|
||||
pool *pond.WorkerPool
|
||||
}
|
||||
)
|
||||
|
||||
func New(o QueueOpts) *Queue {
|
||||
return &Queue{
|
||||
logg: o.Logg,
|
||||
processChan: make(chan uint64, 17_280),
|
||||
stopSignal: make(chan interface{}),
|
||||
processor: o.Processor,
|
||||
pool: o.Pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) Stop() {
|
||||
q.stopSignal <- struct{}{}
|
||||
}
|
||||
|
||||
func (q *Queue) Process() {
|
||||
for {
|
||||
select {
|
||||
case <-q.stopSignal:
|
||||
q.logg.Info("shutdown signal received stopping queue processing")
|
||||
return
|
||||
case block, ok := <-q.processChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
q.pool.Submit(func() {
|
||||
err := q.processor.ProcessBlock(context.Background(), block)
|
||||
if err != nil {
|
||||
q.logg.Error("block processor error", "block_number", block, "error", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) Push(block uint64) {
|
||||
q.processChan <- block
|
||||
}
|
||||
|
||||
func (q *Queue) Size() int {
|
||||
return len(q.processChan)
|
||||
}
|
||||
78
internal/stats/stats.go
Normal file
78
internal/stats/stats.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/cache"
|
||||
)
|
||||
|
||||
type (
|
||||
StatsOpts struct {
|
||||
Cache cache.Cache
|
||||
Logg *slog.Logger
|
||||
Pool *pond.WorkerPool
|
||||
}
|
||||
|
||||
Stats struct {
|
||||
cache cache.Cache
|
||||
logg *slog.Logger
|
||||
pool *pond.WorkerPool
|
||||
stopCh chan struct{}
|
||||
|
||||
latestBlock atomic.Uint64
|
||||
}
|
||||
)
|
||||
|
||||
const statsPrinterInterval = 5 * time.Second
|
||||
|
||||
func New(o StatsOpts) *Stats {
|
||||
return &Stats{
|
||||
cache: o.Cache,
|
||||
logg: o.Logg,
|
||||
pool: o.Pool,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) SetLatestBlock(v uint64) {
|
||||
s.latestBlock.Store(v)
|
||||
}
|
||||
|
||||
func (s *Stats) GetLatestBlock() uint64 {
|
||||
return s.latestBlock.Load()
|
||||
}
|
||||
|
||||
func (s *Stats) Stop() {
|
||||
s.stopCh <- struct{}{}
|
||||
}
|
||||
|
||||
func (s *Stats) APIStatsResponse() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"latestBlock": s.GetLatestBlock(),
|
||||
"poolQueueSize": s.pool.WaitingTasks(),
|
||||
"poolActiveWorkers": s.pool.RunningWorkers(),
|
||||
"cacheSize": s.cache.Size(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) StartStatsPrinter() {
|
||||
ticker := time.NewTicker(statsPrinterInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
s.logg.Debug("stats shutting down")
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.logg.Info("block stats",
|
||||
"latest_block", s.GetLatestBlock(),
|
||||
"pool_queue_size", s.pool.WaitingTasks(),
|
||||
"pool_active_workers", s.pool.RunningWorkers(),
|
||||
"cache_size", s.cache.Size(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
internal/syncer/realtime.go
Normal file
80
internal/syncer/realtime.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/celo-org/celo-blockchain"
|
||||
"github.com/celo-org/celo-blockchain/core/types"
|
||||
"github.com/celo-org/celo-blockchain/event"
|
||||
)
|
||||
|
||||
type BlockQueueFn func(uint64) error
|
||||
|
||||
const resubscribeInterval = 2 * time.Second
|
||||
|
||||
func (s *Syncer) Stop() {
|
||||
if s.realtimeSub != nil {
|
||||
s.realtimeSub.Unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syncer) Start() {
|
||||
s.realtimeSub = event.ResubscribeErr(resubscribeInterval, s.resubscribeFn())
|
||||
}
|
||||
|
||||
func (s *Syncer) receiveRealtimeBlocks(ctx context.Context, fn BlockQueueFn) (celo.Subscription, error) {
|
||||
newHeadersReceiver := make(chan *types.Header, 1)
|
||||
sub, err := s.ethClient.SubscribeNewHead(ctx, newHeadersReceiver)
|
||||
s.logg.Info("realtime syncer connected to ws endpoint")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
eventsCtx, eventsCancel := context.WithCancel(context.Background())
|
||||
defer eventsCancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-quit:
|
||||
s.logg.Info("realtime syncer stopping")
|
||||
eventsCancel()
|
||||
case <-eventsCtx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case header := <-newHeadersReceiver:
|
||||
if err := fn(header.Number.Uint64()); err != nil {
|
||||
s.logg.Error("realtime block queuer error", "error", err)
|
||||
}
|
||||
case <-eventsCtx.Done():
|
||||
s.logg.Info("realtime syncer shutting down")
|
||||
return nil
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
}
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Syncer) queueRealtimeBlock(blockNumber uint64) error {
|
||||
s.queue.Push(blockNumber)
|
||||
if err := s.db.SetUpperBound(blockNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
s.stats.SetLatestBlock(blockNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Syncer) resubscribeFn() event.ResubscribeErrFunc {
|
||||
return func(ctx context.Context, err error) (event.Subscription, error) {
|
||||
if err != nil {
|
||||
s.logg.Error("resubscribing after failed subscription", "error", err)
|
||||
}
|
||||
return s.receiveRealtimeBlocks(ctx, s.queueRealtimeBlock)
|
||||
}
|
||||
}
|
||||
77
internal/syncer/syncer.go
Normal file
77
internal/syncer/syncer.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/celo-org/celo-blockchain"
|
||||
"github.com/celo-org/celo-blockchain/ethclient"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/chain"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/db"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/queue"
|
||||
"github.com/grassrootseconomics/celo-tracker/internal/stats"
|
||||
)
|
||||
|
||||
type (
|
||||
SyncerOpts struct {
|
||||
DB db.DB
|
||||
Chain chain.Chain
|
||||
Logg *slog.Logger
|
||||
Queue *queue.Queue
|
||||
Stats *stats.Stats
|
||||
StartBlock int64
|
||||
WebSocketEndpoint string
|
||||
}
|
||||
|
||||
Syncer struct {
|
||||
db db.DB
|
||||
ethClient *ethclient.Client
|
||||
logg *slog.Logger
|
||||
realtimeSub celo.Subscription
|
||||
stats *stats.Stats
|
||||
queue *queue.Queue
|
||||
stopCh chan struct{}
|
||||
}
|
||||
)
|
||||
|
||||
func New(o SyncerOpts) (*Syncer, error) {
|
||||
latestBlock, err := o.Chain.GetLatestBlock(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lowerBound, err := o.DB.GetLowerBound()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lowerBound == 0 {
|
||||
if o.StartBlock > 0 {
|
||||
if err := o.DB.SetLowerBound(uint64(o.StartBlock)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := o.DB.SetLowerBound(latestBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.DB.SetUpperBound(latestBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Stats.SetLatestBlock(latestBlock)
|
||||
|
||||
ethClient, err := ethclient.Dial(o.WebSocketEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Syncer{
|
||||
db: o.DB,
|
||||
ethClient: ethClient,
|
||||
logg: o.Logg,
|
||||
stats: o.Stats,
|
||||
queue: o.Queue,
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user