254 lines
6.0 KiB
Go

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