Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eventindexer): erc20 indexer api #17519

Merged
merged 14 commits into from
Jun 11, 2024
8 changes: 4 additions & 4 deletions packages/eventindexer/.l2.env
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
HTTP_PORT=4009
METRICS_HTTP_PORT=6067
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_USER=root
DATABASE_PASSWORD=root
DATABASE_NAME=eventindexer
DATABASE_HOST=localhost:3306
DATABASE_MAX_IDLE_CONNS=50
DATABASE_MAX_OPEN_CONNS=3000
DATABASE_CONN_MAX_LIFETIME_IN_MS=100000
RPC_URL=wss://ws.katla.taiko.xyz
RPC_URL=wss://ws.mainnet.taiko.xyz
CORS_ORIGINS=*
BLOCK_BATCH_SIZE=50
CACHE_INTERVAL_IN_SECONDS=60
LAYER=l2
INDEX_NFTS=true
INDEX_ERC20S=true
18 changes: 12 additions & 6 deletions packages/eventindexer/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,24 @@ func InitFromConfig(ctx context.Context, api *API, cfg *Config) error {
return err
}

erc20BalanceRepository, err := repo.NewERC20BalanceRepository(db)
if err != nil {
return err
}

ethClient, err := ethclient.Dial(cfg.RPCUrl)
if err != nil {
return err
}

srv, err := http.NewServer(http.NewServerOpts{
EventRepo: eventRepository,
NFTBalanceRepo: nftBalanceRepository,
ChartRepo: chartRepository,
Echo: echo.New(),
CorsOrigins: cfg.CORSOrigins,
EthClient: ethClient,
EventRepo: eventRepository,
NFTBalanceRepo: nftBalanceRepository,
ERC20BalanceRepo: erc20BalanceRepository,
ChartRepo: chartRepository,
Echo: echo.New(),
CorsOrigins: cfg.CORSOrigins,
EthClient: ethClient,
})
if err != nil {
return err
Expand Down
8 changes: 8 additions & 0 deletions packages/eventindexer/cmd/flags/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ var (
Category: indexerCategory,
EnvVars: []string{"INDEX_NFTS"},
}
IndexERC20s = &cli.BoolFlag{
Name: "indexERC20s",
Usage: "Whether to index erc20 transfer events or not",
Required: false,
Category: indexerCategory,
EnvVars: []string{"INDEX_ERC20S"},
}
)

var IndexerFlags = MergeFlags(CommonFlags, []cli.Flag{
Expand All @@ -87,4 +94,5 @@ var IndexerFlags = MergeFlags(CommonFlags, []cli.Flag{
SubscriptionBackoff,
SyncMode,
IndexNFTs,
IndexERC20s,
})
57 changes: 57 additions & 0 deletions packages/eventindexer/erc20_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package eventindexer

import (
"context"
"net/http"

"github.com/morkid/paginate"
)

type ERC20Metadata struct {
ID int `json:"id"`
ChainID int64 `json:"chainID"`
ContractAddress string `json:"contractAddress"`
Symbol string `json:"symbol"`
Decimals uint8 `json:"decimals"`
}

// ERC20Balance
type ERC20Balance struct {
ID int `json:"id"`
ERC20MetadataID int64 `json:"erc20MetadataID"`
ChainID int64 `json:"chainID"`
Address string `json:"address"`
Amount string `json:"amount"`
ContractAddress string `json:"contractAddress"`
Metadata *ERC20Metadata `json:"metadata" gorm:"foreignKey:ERC20MetadataID"`
}

type UpdateERC20BalanceOpts struct {
ERC20MetadataID int64
ChainID int64
Address string
ContractAddress string
Amount string
}

// ERC20BalanceRepository is used to interact with nft balances in the store
type ERC20BalanceRepository interface {
IncreaseAndDecreaseBalancesInTx(
ctx context.Context,
increaseOpts UpdateERC20BalanceOpts,
decreaseOpts UpdateERC20BalanceOpts,
) (increasedBalance *ERC20Balance, decreasedBalance *ERC20Balance, err error)
FindByAddress(ctx context.Context,
req *http.Request,
address string,
chainID string,
) (paginate.Page, error)
FindMetadata(ctx context.Context, chainID int64, contractAddress string) (*ERC20Metadata, error)
CreateMetadata(
ctx context.Context,
chainID int64,
contractAddress string,
symbol string,
decimals uint8,
) (int, error)
}
2 changes: 2 additions & 0 deletions packages/eventindexer/indexer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Config struct {
SubscriptionBackoff uint64
SyncMode SyncMode
IndexNFTs bool
IndexERC20s bool
Layer string
OpenDBFunc func() (DB, error)
}
Expand All @@ -60,6 +61,7 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
RPCUrl: c.String(flags.IndexerRPCUrl.Name),
SyncMode: SyncMode(c.String(flags.SyncMode.Name)),
IndexNFTs: c.Bool(flags.IndexNFTs.Name),
IndexERC20s: c.Bool(flags.IndexERC20s.Name),
Layer: c.String(flags.Layer.Name),
OpenDBFunc: func() (DB, error) {
return db.OpenDBConnection(db.DBConnectionOpts{
Expand Down
240 changes: 240 additions & 0 deletions packages/eventindexer/indexer/index_erc20_transfers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package indexer

import (
"context"
"fmt"
"math/big"
"strings"

"log/slog"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/pkg/errors"
"github.com/taikoxyz/taiko-mono/packages/eventindexer"
)

// nolint: lll
const erc20ABI = `[{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"}]`

// nolint: lll
const transferEventABI = `[{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]`

// indexERc20Transfers indexes from a given starting block to a given end block and parses all event logs
// to find ERC20 transfer events and update balances
func (i *Indexer) indexERC20Transfers(
ctx context.Context,
chainID *big.Int,
logs []types.Log,
) error {
for _, vLog := range logs {
if !i.isERC20Transfer(ctx, vLog) {
continue
}

if err := i.saveERC20Transfer(ctx, chainID, vLog); err != nil {
return err
}
}

return nil
}

// isERC20Transfer determines whether a given log is a valid ERC20 transfer event
func (i *Indexer) isERC20Transfer(ctx context.Context, vLog types.Log) bool {
// malformed event
if len(vLog.Topics) == 0 {
return false
}

// the first topic is ALWAYS the hash of the event signature.
// this is how people are expected to look up which event is which.
if vLog.Topics[0].Hex() != logTransferSigHash.Hex() {
return false
}

// erc20 transfer length will be 3, nft will be 4, only way to
// differentiate them
if len(vLog.Topics) != 3 {
return false
}

return true
}

// saveERC20Transfer updates the user's balances on the from and to of a ERC20 transfer event
func (i *Indexer) saveERC20Transfer(ctx context.Context, chainID *big.Int, vLog types.Log) error {
from := fmt.Sprintf("0x%v", common.Bytes2Hex(vLog.Topics[1].Bytes()[12:]))

to := fmt.Sprintf("0x%v", common.Bytes2Hex(vLog.Topics[2].Bytes()[12:]))

event := struct {
From common.Address
To common.Address
Value *big.Int
}{}

// Parse the Transfer event ABI
parsedABI, err := abi.JSON(strings.NewReader(transferEventABI))
if err != nil {
return errors.Wrap(err, "abi.JSON(strings.NewReader")
}

err = parsedABI.UnpackIntoInterface(&event, "Transfer", vLog.Data)
if err != nil {
return errors.Wrap(err, "parsedABI.UnpackIntoInterface")
}

amount := event.Value.String()

slog.Info(
"erc20 transfer found",
"from", from,
"to", to,
"amount", event.Value.String(),
"contractAddress", vLog.Address.Hex(),
)

var pk int = 0

md, err := i.erc20BalanceRepo.FindMetadata(ctx, chainID.Int64(), vLog.Address.Hex())
if err != nil {
return errors.Wrap(err, "i.erc20BalanceRepo")
}

if md != nil {
pk = md.ID
}

if pk == 0 {
symbol, err := getERC20Symbol(ctx, i.ethClient, vLog.Address.Hex())
if err != nil {
// some erc20 dont have symbol method properly,
// returns `invalid opcode`.
if strings.Contains(err.Error(), "invalid opcode") {
symbol = "ERC20"
} else {
return errors.Wrap(err, "getERC20Symbol")
}
}

decimals, err := getERC20Decimals(ctx, i.ethClient, vLog.Address.Hex())
if err != nil {
return errors.Wrap(err, "getERC20Decimals")
}

pk, err = i.erc20BalanceRepo.CreateMetadata(ctx, chainID.Int64(), vLog.Address.Hex(), symbol, decimals)
if err != nil {
return errors.Wrap(err, "i.erc20BalanceRepo.CreateMetadata")
}

slog.Info("metadata created", "pk", pk, "symbol", symbol, "decimals", decimals, "contractAddress", vLog.Address.Hex())
} else {
slog.Info("metadata found", "pk", pk, "symbol", md.Symbol, "decimals", md.Decimals, "contractAddress", vLog.Address.Hex())
}

// increment To address's balance
// decrement From address's balance
increaseOpts := eventindexer.UpdateERC20BalanceOpts{
ERC20MetadataID: int64(pk),
ChainID: chainID.Int64(),
Address: to,
ContractAddress: vLog.Address.Hex(),
Amount: amount,
}

decreaseOpts := eventindexer.UpdateERC20BalanceOpts{}

// ignore zero address since that is usually the "mint"
if from != ZeroAddress.Hex() {
decreaseOpts = eventindexer.UpdateERC20BalanceOpts{
ERC20MetadataID: int64(pk),
ChainID: chainID.Int64(),
Address: from,
ContractAddress: vLog.Address.Hex(),
Amount: amount,
}
}

_, _, err = i.erc20BalanceRepo.IncreaseAndDecreaseBalancesInTx(ctx, increaseOpts, decreaseOpts)
if err != nil {
return errors.Wrap(err, "i.erc20BalanceRepo.IncreaseAndDecreaseBalancesInTx")
}

return nil
}

func getERC20Symbol(ctx context.Context, client *ethclient.Client, contractAddress string) (string, error) {
// Parse the contract address
address := common.HexToAddress(contractAddress)

// Parse the ERC20 contract ABI
parsedABI, err := abi.JSON(strings.NewReader(erc20ABI))
if err != nil {
return "", errors.Wrap(err, "abi.JSON")
}

// Prepare the call message
callData, err := parsedABI.Pack("symbol")
if err != nil {
return "", errors.Wrap(err, "parsedABI.Pack")
}

msg := ethereum.CallMsg{
To: &address,
Data: callData,
}

result, err := client.CallContract(ctx, msg, nil)
if err != nil {
return "", errors.Wrap(err, "client.CallContract")
}

var symbol string

err = parsedABI.UnpackIntoInterface(&symbol, "symbol", result)
if err != nil {
return "", errors.Wrap(err, "parsedABI.UnpackIntoInterface")
}

return symbol, nil
}

func getERC20Decimals(ctx context.Context, client *ethclient.Client, contractAddress string) (uint8, error) {
// Parse the contract address
address := common.HexToAddress(contractAddress)

// Parse the ERC20 contract ABI
parsedABI, err := abi.JSON(strings.NewReader(erc20ABI))
if err != nil {
return 0, err
}

// Prepare the call message
callData, err := parsedABI.Pack("decimals")
if err != nil {
return 0, err
}

msg := ethereum.CallMsg{
To: &address,
Data: callData,
}

result, err := client.CallContract(ctx, msg, nil)
if err != nil {
return 0, err
}

var decimals uint8

err = parsedABI.UnpackIntoInterface(&decimals, "decimals", result)
if err != nil {
return 0, err
}

return decimals, nil
}
Loading
Loading