Files
2025-08-02 13:27:31 -05:00

334 lines
9.5 KiB
Go

package api
import (
"context"
"fmt"
"math/big"
"sync"
"github.com/luxfi/geth"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/ethclient"
"github.com/luxfi/evm/iface"
"github.com/luxfi/geth/common"
)
// Interface compliance
var _ EthClient = &ethClient{}
type EthClient interface {
Close()
SendTransaction(context.Context, *types.Transaction) error
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
BalanceAt(context.Context, common.Address, *big.Int) (*big.Int, error)
BlockByNumber(context.Context, *big.Int) (*types.Block, error)
BlockByHash(context.Context, common.Hash) (*types.Block, error)
BlockNumber(context.Context) (uint64, error)
CallContract(context.Context, iface.CallMsg, *big.Int) ([]byte, error)
NonceAt(context.Context, common.Address, *big.Int) (uint64, error)
SuggestGasPrice(context.Context) (*big.Int, error)
AcceptedCodeAt(context.Context, common.Address) ([]byte, error)
AcceptedNonceAt(context.Context, common.Address) (uint64, error)
CodeAt(context.Context, common.Address, *big.Int) ([]byte, error)
EstimateGas(context.Context, iface.CallMsg) (uint64, error)
AcceptedCallContract(context.Context, iface.CallMsg) ([]byte, error)
HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
SuggestGasTipCap(context.Context) (*big.Int, error)
FilterLogs(context.Context, iface.FilterQuery) ([]types.Log, error)
SubscribeFilterLogs(context.Context, iface.FilterQuery, chan<- types.Log) (iface.Subscription, error)
}
// ethClient websocket ethclient.Client with mutexed api calls and lazy conn (on first call)
// All calls are wrapped in a mutex, and try to create a connection if it doesn't exist yet
type ethClient struct {
ipAddr string
chainID string
port uint
client *ethclient.Client
lock sync.Mutex
}
// NewEthClient mainly takes ip/port info for usage in future calls
// Connection can't be initialized in constructor because node is not ready when the constructor is called
// It follows convention of most node api constructors that can be called without having a ready node
func NewEthClient(ipAddr string, port uint) EthClient {
// default to using the C chain
return NewEthClientWithChainID(ipAddr, port, "C")
}
// NewEthClientWithChainID creates an EthClient initialized to connect to
// ipAddr/port and communicate with the given chainID.
func NewEthClientWithChainID(ipAddr string, port uint, chainID string) EthClient {
return &ethClient{
ipAddr: ipAddr,
port: port,
chainID: chainID,
}
}
// connect attempts to connect with websocket ethclient API
func (c *ethClient) connect() error {
if c.client == nil {
client, err := ethclient.Dial(fmt.Sprintf("ws://%s:%d/ext/bc/%s/ws", c.ipAddr, c.port, c.chainID))
if err != nil {
return err
}
c.client = client
}
return nil
}
// Close closes opened connection (if any)
func (c *ethClient) Close() {
if c.client == nil {
return
}
c.lock.Lock()
defer c.lock.Unlock()
c.client.Close()
}
func (c *ethClient) SendTransaction(ctx context.Context, tx *types.Transaction) error {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return err
}
return c.client.SendTransaction(ctx, tx)
}
func (c *ethClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.TransactionReceipt(ctx, txHash)
}
func (c *ethClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BalanceAt(ctx, account, blockNumber)
}
func (c *ethClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BlockByNumber(ctx, number)
}
func (c *ethClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.BlockByHash(ctx, hash)
}
func (c *ethClient) BlockNumber(ctx context.Context) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
return c.client.BlockNumber(ctx)
}
func (c *ethClient) CallContract(ctx context.Context, msg iface.CallMsg, blockNumber *big.Int) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Convert iface.CallMsg to ethereum.CallMsg
ethMsg := ethereum.CallMsg{
From: msg.From,
To: msg.To,
Gas: msg.Gas,
GasPrice: msg.GasPrice,
GasFeeCap: msg.GasFeeCap,
GasTipCap: msg.GasTipCap,
Value: msg.Value,
Data: msg.Data,
}
// Convert AccessList if present
if msg.AccessList != nil {
ethMsg.AccessList = make(types.AccessList, len(msg.AccessList))
for i, tuple := range msg.AccessList {
ethMsg.AccessList[i] = types.AccessTuple{
Address: tuple.Address,
StorageKeys: tuple.StorageKeys,
}
}
}
return c.client.CallContract(ctx, ethMsg, blockNumber)
}
func (c *ethClient) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
return c.client.NonceAt(ctx, account, blockNumber)
}
func (c *ethClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.SuggestGasPrice(ctx)
}
func (c *ethClient) AcceptedCodeAt(ctx context.Context, account common.Address) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Use CodeAt with nil block number to get the latest accepted code
return c.client.CodeAt(ctx, account, nil)
}
func (c *ethClient) AcceptedNonceAt(ctx context.Context, account common.Address) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
// Use NonceAt with nil block number to get the latest accepted nonce
return c.client.NonceAt(ctx, account, nil)
}
func (c *ethClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.CodeAt(ctx, account, blockNumber)
}
func (c *ethClient) EstimateGas(ctx context.Context, msg iface.CallMsg) (uint64, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return 0, err
}
// Convert iface.CallMsg to ethereum.CallMsg
ethMsg := ethereum.CallMsg{
From: msg.From,
To: msg.To,
Gas: msg.Gas,
GasPrice: msg.GasPrice,
GasFeeCap: msg.GasFeeCap,
GasTipCap: msg.GasTipCap,
Value: msg.Value,
Data: msg.Data,
}
// Convert AccessList if present
if msg.AccessList != nil {
ethMsg.AccessList = make(types.AccessList, len(msg.AccessList))
for i, tuple := range msg.AccessList {
ethMsg.AccessList[i] = types.AccessTuple{
Address: tuple.Address,
StorageKeys: tuple.StorageKeys,
}
}
}
return c.client.EstimateGas(ctx, ethMsg)
}
func (c *ethClient) AcceptedCallContract(ctx context.Context, call iface.CallMsg) ([]byte, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Use CallContract with nil block number to get the latest accepted result
ethMsg := ethereum.CallMsg{
From: call.From,
To: call.To,
Gas: call.Gas,
GasPrice: call.GasPrice,
GasFeeCap: call.GasFeeCap,
GasTipCap: call.GasTipCap,
Value: call.Value,
Data: call.Data,
}
// Convert AccessList if present
if call.AccessList != nil {
ethMsg.AccessList = make(types.AccessList, len(call.AccessList))
for i, tuple := range call.AccessList {
ethMsg.AccessList[i] = types.AccessTuple{
Address: tuple.Address,
StorageKeys: tuple.StorageKeys,
}
}
}
return c.client.CallContract(ctx, ethMsg, nil)
}
func (c *ethClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.HeaderByNumber(ctx, number)
}
func (c *ethClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
return c.client.SuggestGasTipCap(ctx)
}
func (c *ethClient) FilterLogs(ctx context.Context, query iface.FilterQuery) ([]types.Log, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Convert iface.FilterQuery to ethereum.FilterQuery
ethQuery := ethereum.FilterQuery{
BlockHash: query.BlockHash,
FromBlock: query.FromBlock,
ToBlock: query.ToBlock,
Addresses: query.Addresses,
Topics: query.Topics,
}
return c.client.FilterLogs(ctx, ethQuery)
}
func (c *ethClient) SubscribeFilterLogs(ctx context.Context, query iface.FilterQuery, ch chan<- types.Log) (iface.Subscription, error) {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.connect(); err != nil {
return nil, err
}
// Convert iface.FilterQuery to ethereum.FilterQuery
ethQuery := ethereum.FilterQuery{
BlockHash: query.BlockHash,
FromBlock: query.FromBlock,
ToBlock: query.ToBlock,
Addresses: query.Addresses,
Topics: query.Topics,
}
return c.client.SubscribeFilterLogs(ctx, ethQuery, ch)
}