142 lines
3.5 KiB
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")
|
|
}
|
|
}
|