package ethereum import ( "context" "crypto/ecdsa" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" ) type EthereumClient interface { GetEthClient() *ethclient.Client GetRPCClient() *rpc.Client Close() IsConnected(ctx context.Context) error NewBoundContract(address common.Address, abi string) (*bind.BoundContract, error) // Creates options for sending transactions to the blockchain. CreateTransactOpts(ctx context.Context, privateKey string, value *big.Int) (*bind.TransactOpts, error) // Creates options for reading from the blockchain. CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts // Waits for a transaction to be mined and returns the receipt. WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error) } type Geth struct { ethClient *ethclient.Client rpcClient *rpc.Client url string } type GethConfig struct { URL string RequestTimeout time.Duration } func DefaultGethConfig() *GethConfig { return &GethConfig{ RequestTimeout: 30 * time.Second, } } func NewGeth(clientURL string) (*Geth, error) { if clientURL == "" { return nil, fmt.Errorf("client URL cannot be empty") } rpcClient, err := rpc.Dial(clientURL) if err != nil { return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", clientURL, err) } ethClient := ethclient.NewClient(rpcClient) geth := &Geth{ ethClient: ethClient, rpcClient: rpcClient, url: clientURL, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := geth.IsConnected(ctx); err != nil { geth.Close() return nil, fmt.Errorf("connection test failed: %w", err) } return geth, nil } func NewGethWithConfig(config *GethConfig) (*Geth, error) { if config == nil { return nil, fmt.Errorf("config cannot be nil") } if config.URL == "" { return nil, fmt.Errorf("client URL cannot be empty") } rpcClient, err := rpc.Dial(config.URL) if err != nil { return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", config.URL, err) } ethClient := ethclient.NewClient(rpcClient) geth := &Geth{ ethClient: ethClient, rpcClient: rpcClient, url: config.URL, } ctx, cancel := context.WithTimeout(context.Background(), config.RequestTimeout) defer cancel() if err := geth.IsConnected(ctx); err != nil { geth.Close() return nil, fmt.Errorf("connection test failed: %w", err) } return geth, nil } func (g *Geth) GetEthClient() *ethclient.Client { return g.ethClient } func (g *Geth) GetRPCClient() *rpc.Client { return g.rpcClient } func (g *Geth) Close() { if g.rpcClient != nil { g.rpcClient.Close() } } func (g *Geth) IsConnected(ctx context.Context) error { if g.ethClient == nil { return fmt.Errorf("ethereum client is not initialized") } _, err := g.ethClient.NetworkID(ctx) if err != nil { return fmt.Errorf("failed to get network ID: %w", err) } return nil } func (g *Geth) GetNetworkID(ctx context.Context) (uint64, error) { if g.ethClient == nil { return 0, fmt.Errorf("ethereum client is not initialized") } networkID, err := g.ethClient.NetworkID(ctx) if err != nil { return 0, fmt.Errorf("failed to get network ID: %w", err) } return networkID.Uint64(), nil } func (g *Geth) GetChainID(ctx context.Context) (uint64, error) { if g.ethClient == nil { return 0, fmt.Errorf("ethereum client is not initialized") } chainID, err := g.ethClient.ChainID(ctx) if err != nil { return 0, fmt.Errorf("failed to get chain ID: %w", err) } return chainID.Uint64(), nil } func (g *Geth) GetBlockNumber(ctx context.Context) (uint64, error) { if g.ethClient == nil { return 0, fmt.Errorf("ethereum client is not initialized") } blockNumber, err := g.ethClient.BlockNumber(ctx) if err != nil { return 0, fmt.Errorf("failed to get block number: %w", err) } return blockNumber, nil } func (g *Geth) GetURL() string { return g.url } func (g *Geth) NewBoundContract(address common.Address, abiJSON string) (*bind.BoundContract, error) { if g.ethClient == nil { return nil, fmt.Errorf("ethereum client is not initialized") } parsedABI, err := parseABI(abiJSON) if err != nil { return nil, fmt.Errorf("failed to parse ABI: %w", err) } return bind.NewBoundContract(address, parsedABI, g.ethClient, g.ethClient, g.ethClient), nil } func (g *Geth) CreateTransactOpts(ctx context.Context, privateKeyHex string, value *big.Int) (*bind.TransactOpts, error) { if g.ethClient == nil { return nil, fmt.Errorf("ethereum client is not initialized") } privateKey, err := parsePrivateKey(privateKeyHex) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } chainID, err := g.ethClient.ChainID(ctx) if err != nil { return nil, fmt.Errorf("failed to get chain ID: %w", err) } opts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) if err != nil { return nil, fmt.Errorf("failed to create transactor: %w", err) } opts.Context = ctx if value != nil { opts.Value = value } return opts, nil } func (g *Geth) CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts { return &bind.CallOpts{ Context: ctx, BlockNumber: blockNumber, } } func (g *Geth) WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { if g.ethClient == nil { return nil, fmt.Errorf("ethereum client is not initialized") } receipt, err := bind.WaitMined(ctx, g.ethClient, &types.Transaction{}) if err != nil { return nil, fmt.Errorf("failed to wait for transaction: %w", err) } return receipt, nil } func parseABI(abiJSON string) (abi.ABI, error) { return abi.JSON(strings.NewReader(abiJSON)) } func parsePrivateKey(privateKeyHex string) (*ecdsa.PrivateKey, error) { privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x") return crypto.HexToECDSA(privateKeyHex) }