mirror of
https://github.com/luxfi/netrunner.git
synced 2026-10-30 15:24:46 +00:00
334 lines
9.5 KiB
Go
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 = ðClient{}
|
|
|
|
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 ðClient{
|
|
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)
|
|
}
|