Files
zeekayandHanzo Dev da299df77d the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:05:15 -07:00

770 lines
22 KiB
Go

// Copyright (C) 2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
// Package evm provides EVM chain interaction and transaction utilities.
package evm
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"math/big"
"net/url"
"strings"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/crypto"
"github.com/luxfi/evm/accounts/abi/bind"
"github.com/luxfi/evm/ethclient"
evmParams "github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/upgrade/legacy"
evmWarp "github.com/luxfi/evm/precompile/contracts/warp"
"github.com/luxfi/evm/predicate"
evmUtils "github.com/luxfi/evm/utils"
ethereum "github.com/luxfi/geth"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/params"
"github.com/luxfi/warp"
)
const (
repeatsOnFailure = 3
baseFeeFactor = 2
maxPriorityFeePerGas = 2500000000 // 2.5 gwei
nativeTransferGas uint64 = 21_000
)
// also used at mocks
var (
ethclientDialContext = ethclient.DialContext
sleepBetweenRepeats = 1 * time.Second
)
// Client wraps over ethclient for calls used by SDK. Features:
// - 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
}
// HexToAddress converts hex string to common.Address (geth type)
func HexToAddress(s string) common.Address {
// Remove 0x prefix if present
if len(s) >= 2 && s[0:2] == "0x" {
s = s[2:]
}
b, _ := hex.DecodeString(s)
return common.BytesToAddress(b)
}
// cryptoToCommon converts crypto.Address to geth common.Address
func cryptoToCommon(addr crypto.Address) common.Address {
return common.BytesToAddress(addr[:])
}
// cryptoToCommonPtr converts crypto.Address to *common.Address
func cryptoToCommonPtr(addr crypto.Address) *common.Address {
a := cryptoToCommon(addr)
return &a
}
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
}
}
// GetClientWithoutScheme 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 := apiLargeContext()
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
}
// GetClient 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 = retryWithContextGen(
apiLargeContext,
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
}
// Close closes underlying ethclient connection
func (client Client) Close() {
client.EthClient.Close()
}
// ContractAlreadyDeployed 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
}
}
// GetContractBytecode returns the contract bytecode at [contractAddress]
// supports [repeatsOnFailure] failures
func (client Client) GetContractBytecode(
contractAddressStr string,
) ([]byte, error) {
contractAddress := HexToAddress(contractAddressStr)
code, err := retryWithContextGen(
apiLargeContext,
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
}
// GetPrivateKeyBalance 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())
}
// GetAddressBalance returns the balance for [address]
// supports [repeatsOnFailure] failures
func (client Client) GetAddressBalance(
addressStr string,
) (*big.Int, error) {
address := HexToAddress(addressStr)
balance, err := retryWithContextGen(
apiLargeContext,
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
}
// NonceAt returns the nonce at [address]
// supports [repeatsOnFailure] failures
func (client Client) NonceAt(
addressStr string,
) (uint64, error) {
address := HexToAddress(addressStr)
nonce, err := retryWithContextGen(
apiLargeContext,
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
}
// SuggestGasTipCap returns the suggested gas tip
// supports [repeatsOnFailure] failures
func (client Client) SuggestGasTipCap() (*big.Int, error) {
gasTipCap, err := retryWithContextGen(
apiLargeContext,
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
}
// EstimateBaseFee returns the estimated base fee
// supports [repeatsOnFailure] failures
func (client Client) EstimateBaseFee() (*big.Int, error) {
baseFee, err := retryWithContextGen(
apiLargeContext,
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
}
// CalculateTxParams 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
}
// EstimateGasLimit returns the estimated gas limit
// supports [repeatsOnFailure] failures
func (client Client) EstimateGasLimit(
msg ethereum.CallMsg,
) (uint64, error) {
gasLimit, err := retryWithContextGen(
apiLargeContext,
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
}
// GetChainID returns the chain ID
// supports [repeatsOnFailure] failures
func (client Client) GetChainID() (*big.Int, error) {
chainID, err := retryWithContextGen(
apiLargeContext,
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
}
// ChainConfig returns the chain conf
// supports [repeatsOnFailure] failures
func (client Client) ChainConfig() (*evmParams.ChainConfigWithUpgradesJSON, error) {
conf, err := retryWithContextGen(
apiLargeContext,
func(ctx context.Context) (*evmParams.ChainConfigWithUpgradesJSON, error) {
return client.EthClient.ChainConfig(ctx)
},
repeatsOnFailure,
sleepBetweenRepeats,
)
if err != nil {
err = fmt.Errorf("failure getting chain config from %s: %w", client.URL, err)
}
return conf, err
}
// SendTransaction sends [tx]
// supports [repeatsOnFailure] failures
func (client Client) SendTransaction(
tx *types.Transaction,
) error {
_, err := retryWithContextGen(
apiLargeContext,
func(ctx context.Context) (any, error) {
return nil, client.EthClient.SendTransaction(ctx, tx)
},
repeatsOnFailure,
sleepBetweenRepeats,
)
if err != nil {
err = fmt.Errorf("failure sending transaction %#v to %s: %w", tx, client.URL, err)
}
return err
}
// WaitForTransaction 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)
}
// FundAddress 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 := 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
}
// IssueTx encodes [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)
txBytes, err := hex.DecodeString(txStr)
if err != nil {
return err
}
if err := tx.UnmarshalBinary(txBytes); 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
}
// GetTxOptsWithSigner 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)
}
// WaitForEVMBootstrapped 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)
}
// TransactWithWarpMessage 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 crypto.Address,
privateKeyStr string,
warpMessage *warp.Message,
contract crypto.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 == (crypto.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 == (crypto.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: evmWarp.ContractAddress,
StorageKeys: evmUtils.BytesToHashSlice(predicate.PackPredicate(warpMessage.Bytes())),
},
}
msg := ethereum.CallMsg{
From: cryptoToCommon(from),
To: cryptoToCommonPtr(contract),
GasPrice: nil,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Value: value,
Data: callData,
AccessList: 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: cryptoToCommonPtr(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)
}
// BlockByNumber gets block [n]
// supports [repeatsOnFailure] failures
func (client Client) BlockByNumber(n *big.Int) (*types.Block, error) {
block, err := retryWithContextGen(
apiLargeContext,
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
}
// FilterLogs gets logs as given by [query]
// supports [repeatsOnFailure] failures
func (client Client) FilterLogs(query ethereum.FilterQuery) ([]types.Log, error) {
logs, err := retryWithContextGen(
apiLargeContext,
func(ctx context.Context) ([]types.Log, error) {
return client.EthClient.FilterLogs(ctx, query)
},
repeatsOnFailure,
sleepBetweenRepeats,
)
if err != nil {
err = fmt.Errorf("failure retrieving logs on %s: %w", client.URL, err)
}
return logs, err
}
// TransactionReceipt gets tx receipt for [hash]
// supports [repeatsOnFailure] failures
func (client Client) TransactionReceipt(hash common.Hash) (*types.Receipt, error) {
receipt, err := retryWithContextGen(
apiLargeContext,
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
}
// BlockNumber gets current height
// supports [repeatsOnFailure] failures
func (client Client) BlockNumber() (uint64, error) {
blockNumber, err := retryWithContextGen(
apiLargeContext,
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
}
// WaitForNewBlock 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)
}
// CreateDummyBlocks issues 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, cryptoToCommon(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
}
// SetupProposerVM issues 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 := 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
}