mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
804 lines
23 KiB
Go
804 lines
23 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package evm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/v2/sdk/constants"
|
|
"github.com/luxfi/cli/v2/sdk/utils"
|
|
luxWarp "github.com/luxfi/warp"
|
|
"github.com/luxfi/evm/v2/accounts/abi/bind"
|
|
"github.com/luxfi/evm/v2/ethclient"
|
|
"github.com/luxfi/evm/v2/iface"
|
|
"github.com/luxfi/evm/v2/params"
|
|
"github.com/luxfi/evm/v2/upgrade/legacy"
|
|
"github.com/luxfi/evm/v2/precompile/contracts/warp"
|
|
"github.com/luxfi/evm/v2/predicate"
|
|
subnetEvmUtils "github.com/luxfi/evm/v2/utils"
|
|
"github.com/luxfi/evm/v2/core/types"
|
|
"github.com/luxfi/geth/common"
|
|
gethtypes "github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/geth/crypto"
|
|
ethparams "github.com/luxfi/geth/params"
|
|
)
|
|
|
|
const (
|
|
repeatsOnFailure = 3
|
|
baseFeeFactor = 2
|
|
maxPriorityFeePerGas = 2500000000 // 2.5 gwei
|
|
nativeTransferGas uint64 = 21_000
|
|
)
|
|
|
|
// convertAccessList converts evm/core/types.AccessList to iface.AccessList (geth types)
|
|
func convertAccessList(evmList types.AccessList) iface.AccessList {
|
|
// Convert evm AccessList to geth AccessList
|
|
gethList := make(gethtypes.AccessList, len(evmList))
|
|
for i, tuple := range evmList {
|
|
gethList[i] = gethtypes.AccessTuple{
|
|
Address: tuple.Address,
|
|
StorageKeys: tuple.StorageKeys,
|
|
}
|
|
}
|
|
return gethList
|
|
}
|
|
|
|
// also used at mocks
|
|
var (
|
|
ethclientDialContext = ethclient.DialContext
|
|
sleepBetweenRepeats = 1 * time.Second
|
|
)
|
|
|
|
// wraps over ethclient for calls used by SDK. featues:
|
|
// - finds out url scheme in case it is missing, to connect to ws/wss/http/https
|
|
// - repeats to try to recover from failures, generating its own context for each call
|
|
// - logs rpc url in case of failure
|
|
// - receives addresses and private keys as strings
|
|
type Client struct {
|
|
EthClient ethclient.Client
|
|
URL string
|
|
}
|
|
|
|
// indicates if the given rpc url has schema or not
|
|
func HasScheme(rpcURL string) (bool, error) {
|
|
if parsedURL, err := url.Parse(rpcURL); err != nil {
|
|
if !strings.Contains(err.Error(), "first path segment in URL cannot contain colon") {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
} else {
|
|
return strings.Contains(rpcURL, "://") && parsedURL.Scheme != "", nil
|
|
}
|
|
}
|
|
|
|
// tries to connect an ethclient to a rpc url without scheme,
|
|
// by trying out different possible schemes: ws, wss, https, http
|
|
func GetClientWithoutScheme(rpcURL string) (ethclient.Client, string, error) {
|
|
if b, err := HasScheme(rpcURL); err != nil {
|
|
return nil, "", err
|
|
} else if b {
|
|
return nil, "", fmt.Errorf("url does have scheme")
|
|
}
|
|
notDeterminedErr := fmt.Errorf("url %s has no scheme and protocol could not be determined", rpcURL)
|
|
// let's start with ws it always give same error for http/https/wss
|
|
scheme := "ws://"
|
|
ctx, cancel := utils.GetAPILargeContext()
|
|
defer cancel()
|
|
client, err := ethclientDialContext(ctx, scheme+rpcURL)
|
|
if err == nil {
|
|
return client, scheme, nil
|
|
} else if !strings.Contains(err.Error(), "websocket: bad handshake") {
|
|
return nil, "", notDeterminedErr
|
|
}
|
|
// wss give specific errors for http/http
|
|
scheme = "wss://"
|
|
client, err = ethclientDialContext(ctx, scheme+rpcURL)
|
|
if err == nil {
|
|
return client, scheme, nil
|
|
} else if !strings.Contains(err.Error(), "websocket: bad handshake") && // may be https
|
|
!strings.Contains(err.Error(), "first record does not look like a TLS handshake") { // may be http
|
|
return nil, "", notDeterminedErr
|
|
}
|
|
// https/http discrimination based on sending a specific query
|
|
scheme = "https://"
|
|
client, err = ethclientDialContext(ctx, scheme+rpcURL)
|
|
if err == nil {
|
|
_, err = client.ChainID(ctx)
|
|
switch {
|
|
case err == nil:
|
|
return client, scheme, nil
|
|
case strings.Contains(err.Error(), "server gave HTTP response to HTTPS client"):
|
|
scheme = "http://"
|
|
client, err = ethclientDialContext(ctx, scheme+rpcURL)
|
|
if err == nil {
|
|
return client, scheme, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, "", notDeterminedErr
|
|
}
|
|
|
|
// connects an evm client to the given [rpcURL]
|
|
// supports [repeatsOnFailure] failures
|
|
func GetClient(rpcURL string) (Client, error) {
|
|
client := Client{
|
|
URL: rpcURL,
|
|
}
|
|
hasScheme, err := HasScheme(rpcURL)
|
|
if err != nil {
|
|
return client, fmt.Errorf("failure determining the scheme of url %s: %w", rpcURL, err)
|
|
}
|
|
client.EthClient, err = utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (ethclient.Client, error) {
|
|
if hasScheme {
|
|
return ethclientDialContext(ctx, rpcURL)
|
|
} else {
|
|
client, _, err := GetClientWithoutScheme(rpcURL)
|
|
return client, err
|
|
}
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure connecting to %s: %w", rpcURL, err)
|
|
}
|
|
return client, err
|
|
}
|
|
|
|
// closes underlying ethclient connection
|
|
func (client Client) Close() {
|
|
client.EthClient.Close()
|
|
}
|
|
|
|
// indicates wether a contract is deployed on [contractAddress]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) ContractAlreadyDeployed(
|
|
contractAddress string,
|
|
) (bool, error) {
|
|
if bs, err := client.GetContractBytecode(contractAddress); err != nil {
|
|
return false, err
|
|
} else {
|
|
return len(bs) != 0, nil
|
|
}
|
|
}
|
|
|
|
// returns the contract bytecode at [contractAddress]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) GetContractBytecode(
|
|
contractAddressStr string,
|
|
) ([]byte, error) {
|
|
contractAddress := common.HexToAddress(contractAddressStr)
|
|
code, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) ([]byte, error) {
|
|
return client.EthClient.CodeAt(ctx, contractAddress, nil)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"failure obtaining code from %s at address %s: %w",
|
|
client.URL,
|
|
contractAddressStr,
|
|
err,
|
|
)
|
|
}
|
|
return code, err
|
|
}
|
|
|
|
// returns the balance for [privateKey]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) GetPrivateKeyBalance(
|
|
privateKey string,
|
|
) (*big.Int, error) {
|
|
addr, err := PrivateKeyToAddress(privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.GetAddressBalance(addr.Hex())
|
|
}
|
|
|
|
// returns the balance for [address]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) GetAddressBalance(
|
|
addressStr string,
|
|
) (*big.Int, error) {
|
|
address := common.HexToAddress(addressStr)
|
|
balance, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*big.Int, error) {
|
|
return client.EthClient.BalanceAt(ctx, address, nil)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure obtaining balance for %s on %s: %w", addressStr, client.URL, err)
|
|
}
|
|
return balance, err
|
|
}
|
|
|
|
// returns the nonce at [address]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) NonceAt(
|
|
addressStr string,
|
|
) (uint64, error) {
|
|
address := common.HexToAddress(addressStr)
|
|
nonce, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (uint64, error) {
|
|
return client.EthClient.NonceAt(ctx, address, nil)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure obtaining nonce for %s on %s: %w", addressStr, client.URL, err)
|
|
}
|
|
return nonce, err
|
|
}
|
|
|
|
// returns the suggested gas tip
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) SuggestGasTipCap() (*big.Int, error) {
|
|
gasTipCap, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*big.Int, error) {
|
|
return client.EthClient.SuggestGasTipCap(ctx)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure obtaining gas tip cap on %s: %w", client.URL, err)
|
|
}
|
|
return gasTipCap, err
|
|
}
|
|
|
|
// returns the estimated base fee
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) EstimateBaseFee() (*big.Int, error) {
|
|
baseFee, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*big.Int, error) {
|
|
return client.EthClient.EstimateBaseFee(ctx)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure estimating base fee on %s: %w", client.URL, err)
|
|
}
|
|
return baseFee, err
|
|
}
|
|
|
|
// Returns gasFeeCap, gasTipCap, and nonce to be used when constructing a transaction
|
|
// supports [repeatsOnFailure] failures on each step
|
|
func (client Client) CalculateTxParams(
|
|
address string,
|
|
) (*big.Int, *big.Int, uint64, error) {
|
|
baseFee, err := client.EstimateBaseFee()
|
|
if err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
gasTipCap, err := client.SuggestGasTipCap()
|
|
if err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
nonce, err := client.NonceAt(address)
|
|
if err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
gasFeeCap := baseFee.Mul(baseFee, big.NewInt(baseFeeFactor))
|
|
gasFeeCap.Add(gasFeeCap, big.NewInt(maxPriorityFeePerGas))
|
|
return gasFeeCap, gasTipCap, nonce, nil
|
|
}
|
|
|
|
// returns the estimated gas limit
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) EstimateGasLimit(
|
|
msg iface.CallMsg,
|
|
) (uint64, error) {
|
|
gasLimit, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (uint64, error) {
|
|
return client.EthClient.EstimateGas(ctx, msg)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure estimating gas limit on %s: %w", client.URL, err)
|
|
}
|
|
return gasLimit, err
|
|
}
|
|
|
|
// returns the chain ID
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) GetChainID() (*big.Int, error) {
|
|
chainID, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*big.Int, error) {
|
|
return client.EthClient.ChainID(ctx)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure getting chain id from %s: %w", client.URL, err)
|
|
}
|
|
return chainID, err
|
|
}
|
|
|
|
// returns the chain conf
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) ChainConfig() (*params.ChainConfigWithUpgradesJSON, error) {
|
|
// ChainConfig is not available in standard ethclient
|
|
// This would need to be implemented using a custom RPC call
|
|
// For now, return a default config
|
|
chainID, err := client.EthClient.ChainID(context.Background())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failure getting chain ID from %s: %w", client.URL, err)
|
|
}
|
|
|
|
// Create a default config based on chain ID
|
|
// ChainConfigWithUpgradesJSON embeds ChainConfig which embeds ethparams.ChainConfig
|
|
ethConfig := ðparams.ChainConfig{
|
|
ChainID: chainID,
|
|
}
|
|
|
|
config := ¶ms.ChainConfigWithUpgradesJSON{
|
|
ChainConfig: params.ChainConfig{
|
|
ChainConfig: ethConfig,
|
|
},
|
|
UpgradeConfig: params.UpgradeConfig{},
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// sends [tx]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) SendTransaction(
|
|
tx *types.Transaction,
|
|
) error {
|
|
_, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (any, error) {
|
|
// ethclient expects an iface.Transaction (geth types)
|
|
// but we have evm types.Transaction
|
|
// Since direct conversion is not available, serialize and recreate
|
|
data, err := tx.MarshalBinary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create a new geth transaction from the binary data
|
|
gethTx := new(iface.Transaction)
|
|
if err := gethTx.UnmarshalBinary(data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, client.EthClient.SendTransaction(ctx, gethTx)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure sending transaction %#v to %s: %w", tx, client.URL, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// waits for [tx]'s receipt to have successful state
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) WaitForTransaction(
|
|
tx *types.Transaction,
|
|
) (*types.Receipt, bool, error) {
|
|
steps := int(constants.APIRequestLargeTimeout.Seconds())
|
|
var cumErr error
|
|
for step := 0; step < steps; step++ {
|
|
receipt, err := client.TransactionReceipt(tx.Hash())
|
|
if err == nil {
|
|
var success bool
|
|
if receipt != nil {
|
|
success = receipt.Status == types.ReceiptStatusSuccessful
|
|
}
|
|
return receipt, success, err
|
|
}
|
|
cumErr = errors.Join(cumErr, err)
|
|
time.Sleep(sleepBetweenRepeats)
|
|
}
|
|
return nil, false, fmt.Errorf("timeout of %d seconds while waiting for tx %#v on %s: %w", steps, tx, client.URL, cumErr)
|
|
}
|
|
|
|
// transfers [amount] to [targetAddressStr] using [sourceAddressPrivateKeyStr]
|
|
// supports [repeatsOnFailure] failures on each step
|
|
func (client Client) FundAddress(
|
|
sourceAddressPrivateKeyStr string,
|
|
targetAddressStr string,
|
|
amount *big.Int,
|
|
) (*types.Receipt, error) {
|
|
sourceAddressPrivateKey, err := crypto.HexToECDSA(sourceAddressPrivateKeyStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceAddress := crypto.PubkeyToAddress(sourceAddressPrivateKey.PublicKey)
|
|
gasFeeCap, gasTipCap, nonce, err := client.CalculateTxParams(sourceAddress.Hex())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetAddress := common.HexToAddress(targetAddressStr)
|
|
chainID, err := client.GetChainID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx := types.NewTx(&types.DynamicFeeTx{
|
|
ChainID: chainID,
|
|
Nonce: nonce,
|
|
To: &targetAddress,
|
|
Gas: nativeTransferGas,
|
|
GasFeeCap: gasFeeCap,
|
|
GasTipCap: gasTipCap,
|
|
Value: amount,
|
|
})
|
|
txSigner := types.LatestSignerForChainID(chainID)
|
|
signedTx, err := types.SignTx(tx, txSigner, sourceAddressPrivateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := client.SendTransaction(signedTx); err != nil {
|
|
return nil, err
|
|
}
|
|
receipt, b, err := client.WaitForTransaction(signedTx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !b {
|
|
return nil, fmt.Errorf("failure funding %s from %s amount %d", targetAddressStr, sourceAddress.Hex(), amount)
|
|
}
|
|
return receipt, nil
|
|
}
|
|
|
|
// encode [txStr] to binary, sends and waits for it
|
|
// supports [repeatsOnFailure] failures on each step
|
|
func (client Client) IssueTx(
|
|
txStr string,
|
|
) error {
|
|
tx := new(types.Transaction)
|
|
if err := tx.UnmarshalBinary(common.FromHex(txStr)); err != nil {
|
|
return err
|
|
}
|
|
if err := client.SendTransaction(tx); err != nil {
|
|
return err
|
|
}
|
|
if receipt, b, err := client.WaitForTransaction(tx); err != nil {
|
|
return err
|
|
} else if !b {
|
|
return fmt.Errorf("failure sending tx: got status %d expected %d", receipt.Status, types.ReceiptStatusSuccessful)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// returns tx options that include signer for [prefundedPrivateKeyStr]
|
|
// supports [repeatsOnFailure] failures when gathering chain info
|
|
func (client Client) GetTxOptsWithSigner(
|
|
prefundedPrivateKeyStr string,
|
|
) (*bind.TransactOpts, error) {
|
|
prefundedPrivateKey, err := crypto.HexToECDSA(prefundedPrivateKeyStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainID, err := client.GetChainID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failure generating signer: %w", err)
|
|
}
|
|
return bind.NewKeyedTransactorWithChainID(prefundedPrivateKey, chainID)
|
|
}
|
|
|
|
// waits for [timeout] until evm is bootstrapped
|
|
// considers evm is bootstrapped if it responds to an evm call (ChainID)
|
|
func (client Client) WaitForEVMBootstrapped(timeout time.Duration) error {
|
|
if timeout == 0 {
|
|
timeout = 60 * time.Second
|
|
}
|
|
steps := int(timeout.Seconds())
|
|
var cumErr error
|
|
for step := 0; step < steps; step++ {
|
|
if _, err := client.GetChainID(); err == nil {
|
|
return nil
|
|
} else {
|
|
cumErr = errors.Join(cumErr, err)
|
|
}
|
|
time.Sleep(sleepBetweenRepeats)
|
|
}
|
|
return fmt.Errorf("client at %s not bootstrapped after %.2f seconds: %w", client.URL, timeout.Seconds(), cumErr)
|
|
}
|
|
|
|
// generates a transaction signed with [privateKeyStr], calling a [contract] method using [callData]
|
|
// including [warpMessage] in the tx accesslist
|
|
// if [generateRawTxOnly] is set, it generates a similar, unsigned tx, with given [from] address
|
|
func (client Client) TransactWithWarpMessage(
|
|
from common.Address,
|
|
privateKeyStr string,
|
|
warpMessage *luxWarp.Message,
|
|
contract common.Address,
|
|
callData []byte,
|
|
value *big.Int,
|
|
generateRawTxOnly bool,
|
|
) (*types.Transaction, error) {
|
|
const defaultGasLimit = 2_000_000
|
|
var (
|
|
privateKey *ecdsa.PrivateKey
|
|
err error
|
|
)
|
|
if privateKeyStr == "" && from == (common.Address{}) {
|
|
return nil, fmt.Errorf("from address and private key can't be both empty at GetTxToMethodWithWarpMessage")
|
|
}
|
|
if !generateRawTxOnly && privateKeyStr == "" {
|
|
return nil, fmt.Errorf("from private key must be defined to be able to sign the tx at GetTxToMethodWithWarpMessage")
|
|
}
|
|
if privateKeyStr != "" {
|
|
privateKey, err = crypto.HexToECDSA(privateKeyStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if from == (common.Address{}) {
|
|
from = crypto.PubkeyToAddress(privateKey.PublicKey)
|
|
}
|
|
}
|
|
gasFeeCap, gasTipCap, nonce, err := client.CalculateTxParams(from.Hex())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainID, err := client.GetChainID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accessList := types.AccessList{
|
|
types.AccessTuple{
|
|
Address: warp.ContractAddress,
|
|
StorageKeys: subnetEvmUtils.BytesToHashSlice(predicate.PackPredicate(warpMessage.Bytes())),
|
|
},
|
|
}
|
|
msg := iface.CallMsg{
|
|
From: from,
|
|
To: &contract,
|
|
GasPrice: nil,
|
|
GasTipCap: gasTipCap,
|
|
GasFeeCap: gasFeeCap,
|
|
Value: value,
|
|
Data: callData,
|
|
AccessList: convertAccessList(accessList),
|
|
}
|
|
gasLimit, err := client.EstimateGasLimit(msg)
|
|
if err != nil {
|
|
// assuming this is related to the tx itself.
|
|
// just using default gas limit, and let the user debug the
|
|
// tx if needed so
|
|
gasLimit = defaultGasLimit
|
|
}
|
|
tx := types.NewTx(&types.DynamicFeeTx{
|
|
ChainID: chainID,
|
|
Nonce: nonce,
|
|
To: &contract,
|
|
Gas: gasLimit,
|
|
GasFeeCap: gasFeeCap,
|
|
GasTipCap: gasTipCap,
|
|
Value: value,
|
|
Data: callData,
|
|
AccessList: accessList,
|
|
})
|
|
if generateRawTxOnly {
|
|
return tx, nil
|
|
}
|
|
txSigner := types.LatestSignerForChainID(chainID)
|
|
return types.SignTx(tx, txSigner, privateKey)
|
|
}
|
|
|
|
// gets block [n]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) BlockByNumber(n *big.Int) (*types.Block, error) {
|
|
block, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*types.Block, error) {
|
|
return client.EthClient.BlockByNumber(ctx, n)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure retrieving block %d on %s: %w", n, client.URL, err)
|
|
}
|
|
return block, err
|
|
}
|
|
|
|
// get logs as given by [query]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) FilterLogs(query iface.FilterQuery) ([]types.Log, error) {
|
|
logs, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) ([]types.Log, error) {
|
|
// ethclient returns []iface.Log (geth types)
|
|
// but we need []types.Log (evm types)
|
|
gethLogs, err := client.EthClient.FilterLogs(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert geth logs to evm logs
|
|
evmLogs := make([]types.Log, len(gethLogs))
|
|
for i, gethLog := range gethLogs {
|
|
evmLogs[i] = types.Log{
|
|
Address: gethLog.Address,
|
|
Topics: gethLog.Topics,
|
|
Data: gethLog.Data,
|
|
BlockNumber: gethLog.BlockNumber,
|
|
TxHash: gethLog.TxHash,
|
|
TxIndex: gethLog.TxIndex,
|
|
BlockHash: gethLog.BlockHash,
|
|
Index: gethLog.Index,
|
|
Removed: gethLog.Removed,
|
|
}
|
|
}
|
|
return evmLogs, nil
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure retrieving logs on %s: %w", client.URL, err)
|
|
}
|
|
return logs, err
|
|
}
|
|
|
|
// get tx receipt for [hash]
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) TransactionReceipt(hash common.Hash) (*types.Receipt, error) {
|
|
receipt, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (*types.Receipt, error) {
|
|
return client.EthClient.TransactionReceipt(ctx, hash)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure retrieving receipt for %s on %s: %w", hash, client.URL, err)
|
|
}
|
|
return receipt, err
|
|
}
|
|
|
|
// gets current height
|
|
// supports [repeatsOnFailure] failures
|
|
func (client Client) BlockNumber() (uint64, error) {
|
|
blockNumber, err := utils.RetryWithContextGen(
|
|
utils.GetAPILargeContext,
|
|
func(ctx context.Context) (uint64, error) {
|
|
return client.EthClient.BlockNumber(ctx)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure retrieving height (block number) on %s: %w", client.URL, err)
|
|
}
|
|
return blockNumber, err
|
|
}
|
|
|
|
// waits until current height is bigger than the given previous height at [prevBlockNumber]
|
|
// supports [repeatsOnFailure] failures on each step
|
|
func (client Client) WaitForNewBlock(
|
|
prevBlockNumber uint64,
|
|
totalDuration time.Duration,
|
|
) error {
|
|
if totalDuration == 0 {
|
|
totalDuration = 10 * time.Second
|
|
}
|
|
steps := int(totalDuration.Seconds())
|
|
for step := 0; step < steps; step++ {
|
|
blockNumber, err := client.BlockNumber()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if blockNumber > prevBlockNumber {
|
|
return nil
|
|
}
|
|
time.Sleep(sleepBetweenRepeats)
|
|
}
|
|
return fmt.Errorf("no new block produced on %s in %d seconds", client.URL, steps)
|
|
}
|
|
|
|
// issue dummy txs to create the given number of blocks
|
|
func (client Client) CreateDummyBlocks(
|
|
numBlocks int,
|
|
privKeyStr string,
|
|
) error {
|
|
addr, err := PrivateKeyToAddress(privKeyStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privKey, err := crypto.HexToECDSA(privKeyStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chainID, err := client.GetChainID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gasPrice := big.NewInt(legacy.BaseFee)
|
|
txSigner := types.LatestSignerForChainID(chainID)
|
|
blockNumber, err := client.BlockNumber()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get block number: %w", err)
|
|
}
|
|
nonce, err := client.NonceAt(addr.Hex())
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get nonce: %w", err)
|
|
}
|
|
for i := 0; i < numBlocks; i++ {
|
|
// it may be the case that we hit an outdated node with the rpc, so lets not fully trust the API
|
|
if blockNumberFromAPI, err := client.BlockNumber(); err != nil {
|
|
return fmt.Errorf("client.BlockNumber failure at step %d: %w", i, err)
|
|
} else if blockNumberFromAPI > blockNumber {
|
|
// changes from outside
|
|
blockNumber = blockNumberFromAPI
|
|
}
|
|
if nonceFromAPI, err := client.NonceAt(addr.Hex()); err != nil {
|
|
return fmt.Errorf("client.NonceAt failure at step %d: %w", i, err)
|
|
} else if nonceFromAPI > nonce {
|
|
// changes from outside
|
|
nonce = nonceFromAPI
|
|
}
|
|
// send Big1 to himself
|
|
tx := types.NewTransaction(nonce, addr, common.Big1, params.TxGas, gasPrice, nil)
|
|
triggerTx, err := types.SignTx(tx, txSigner, privKey)
|
|
if err != nil {
|
|
return fmt.Errorf("types.SignTx failure at step %d: %w", i, err)
|
|
}
|
|
if err := client.SendTransaction(triggerTx); err != nil {
|
|
return fmt.Errorf("client.SendTransaction failure at step %d: %w", i, err)
|
|
}
|
|
if err := client.WaitForNewBlock(blockNumber, 0); err != nil {
|
|
return fmt.Errorf("WaitForNewBlock failure at step %d: %w", i, err)
|
|
}
|
|
blockNumber++
|
|
nonce++
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// issue transactions on [client] so as to activate Proposer VM Fork
|
|
// this should generate a PostForkBlock because its parent block
|
|
// (genesis) has a timestamp (0) that is greater than or equal to the fork
|
|
// activation time of 0. Therefore, subsequent blocks should be built with
|
|
// BuildBlockWithContext.
|
|
// the current timestamp should be after the ProposerVM activation time (aka ApricotPhase4).
|
|
// supports [repeatsOnFailure] failures on each step
|
|
func (client Client) SetupProposerVM(
|
|
privKey string,
|
|
) error {
|
|
const numBlocks = 2 // Number of blocks needed to activate the proposer VM fork
|
|
_, err := utils.Retry(
|
|
func() (any, error) {
|
|
return nil, client.CreateDummyBlocks(numBlocks, privKey)
|
|
},
|
|
repeatsOnFailure,
|
|
sleepBetweenRepeats,
|
|
)
|
|
if err != nil {
|
|
err = fmt.Errorf("failure issuing tx to activate proposer VM: %w", err)
|
|
}
|
|
return err
|
|
}
|