gcx/backend/services/chain-indexer/internal/infrastructure/rpc/eth_client.go

142 lines
3.5 KiB
Go

package rpc
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"go.uber.org/zap"
"github.com/genex/chain-indexer/internal/domain/entity"
"github.com/genex/chain-indexer/internal/domain/repository"
)
// Compile-time check: EthChainClient implements repository.ChainClient.
var _ repository.ChainClient = (*EthChainClient)(nil)
// EthChainClient implements ChainClient using go-ethereum ethclient.
type EthChainClient struct {
ethClient *ethclient.Client
chainID *big.Int
logger *zap.Logger
}
// NewEthChainClient connects to an EVM JSON-RPC endpoint and returns a ChainClient.
func NewEthChainClient(rpcURL string, logger *zap.Logger) (*EthChainClient, error) {
client, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, fmt.Errorf("failed to dial RPC %s: %w", rpcURL, err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
chainID, err := client.ChainID(ctx)
if err != nil {
client.Close()
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
logger.Info("Connected to blockchain node",
zap.String("rpcURL", rpcURL),
zap.Int64("chainID", chainID.Int64()),
)
return &EthChainClient{
ethClient: client,
chainID: chainID,
logger: logger,
}, nil
}
// GetLatestBlockNumber returns the current head block number.
func (c *EthChainClient) GetLatestBlockNumber(ctx context.Context) (int64, error) {
num, err := c.ethClient.BlockNumber(ctx)
if err != nil {
return 0, fmt.Errorf("eth_blockNumber failed: %w", err)
}
return int64(num), nil
}
// GetBlockByNumber fetches a full block and parses its transactions.
func (c *EthChainClient) GetBlockByNumber(ctx context.Context, height int64) (*entity.Block, []*entity.ChainTransaction, error) {
block, err := c.ethClient.BlockByNumber(ctx, big.NewInt(height))
if err != nil {
return nil, nil, fmt.Errorf("eth_getBlockByNumber(%d) failed: %w", height, err)
}
blockTime := time.Unix(int64(block.Time()), 0)
domainBlock, err := entity.NewBlock(
block.Number().Int64(),
block.Hash().Hex(),
blockTime,
len(block.Transactions()),
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create block entity: %w", err)
}
signer := types.LatestSignerForChainID(c.chainID)
var txs []*entity.ChainTransaction
for _, tx := range block.Transactions() {
from, err := types.Sender(signer, tx)
if err != nil {
c.logger.Warn("Failed to recover tx sender",
zap.String("txHash", tx.Hash().Hex()),
zap.Error(err),
)
continue
}
to := ""
if tx.To() != nil {
to = tx.To().Hex()
}
// Get receipt for tx status
status := "confirmed"
receipt, err := c.ethClient.TransactionReceipt(ctx, tx.Hash())
if err != nil {
c.logger.Warn("Failed to get tx receipt, assuming confirmed",
zap.String("txHash", tx.Hash().Hex()),
zap.Error(err),
)
} else if receipt.Status == 0 {
status = "failed"
}
chainTx, err := entity.NewChainTransaction(
tx.Hash().Hex(),
block.Number().Int64(),
from.Hex(),
to,
tx.Value().String(),
status,
blockTime,
)
if err != nil {
c.logger.Warn("Failed to create tx entity",
zap.String("txHash", tx.Hash().Hex()),
zap.Error(err),
)
continue
}
txs = append(txs, chainTx)
}
return domainBlock, txs, nil
}
// Close releases the underlying ethclient connection.
func (c *EthChainClient) Close() {
if c.ethClient != nil {
c.ethClient.Close()
c.logger.Info("Blockchain RPC connection closed")
}
}