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") } }