254 lines
6.0 KiB
Go
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)
|
|
}
|