github.com/0xsequence/ethkit@v1.25.0/ethtest/testchain.go (about)

     1  package ethtest
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/big"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/0xsequence/ethkit"
    15  	"github.com/0xsequence/ethkit/ethcontract"
    16  	"github.com/0xsequence/ethkit/ethrpc"
    17  	"github.com/0xsequence/ethkit/ethtxn"
    18  	"github.com/0xsequence/ethkit/ethwallet"
    19  	"github.com/0xsequence/ethkit/go-ethereum/accounts/abi/bind"
    20  	"github.com/0xsequence/ethkit/go-ethereum/common"
    21  	"github.com/0xsequence/ethkit/go-ethereum/core/types"
    22  )
    23  
    24  type Testchain struct {
    25  	options TestchainOptions
    26  
    27  	chainID        *big.Int         // chainID determined by the test chain
    28  	walletMnemonic string           // test wallet mnemonic parsed from package.json
    29  	Provider       *ethrpc.Provider // provider rpc to the test chain
    30  }
    31  
    32  type TestchainOptions struct {
    33  	NodeURL string
    34  }
    35  
    36  var DefaultTestchainOptions = TestchainOptions{
    37  	NodeURL: "http://localhost:8545",
    38  }
    39  
    40  func NewTestchain(opts ...TestchainOptions) (*Testchain, error) {
    41  	var err error
    42  	tc := &Testchain{}
    43  
    44  	// set options
    45  	if len(opts) > 0 {
    46  		tc.options = opts[0]
    47  	} else {
    48  		tc.options = DefaultTestchainOptions
    49  	}
    50  
    51  	// provider
    52  	tc.Provider, err = ethrpc.NewProvider(tc.options.NodeURL)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	// connect to the test-chain or error out if fail to communicate
    58  	if err := tc.connect(); err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	return tc, nil
    63  }
    64  
    65  func (c *Testchain) connect() error {
    66  	numAttempts := 6
    67  	for i := 0; i < numAttempts; i++ {
    68  		chainID, err := c.Provider.ChainID(context.Background())
    69  		if err != nil || chainID == nil {
    70  			time.Sleep(1 * time.Second)
    71  			continue
    72  		}
    73  		c.chainID = chainID
    74  	}
    75  	if c.chainID == nil {
    76  		return fmt.Errorf("ethtest: unable to connect to testchain")
    77  	}
    78  	return nil
    79  }
    80  
    81  func (c *Testchain) ChainID() *big.Int {
    82  	return c.chainID
    83  }
    84  
    85  func (c *Testchain) Wallet() (*ethwallet.Wallet, error) {
    86  	var err error
    87  
    88  	if c.walletMnemonic == "" {
    89  		c.walletMnemonic, err = parseTestWalletMnemonic()
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  	}
    94  
    95  	// we create a new instance each time so we don't change the account indexes
    96  	// on the wallet across consumers
    97  	wallet, err := ethwallet.NewWalletFromMnemonic(c.walletMnemonic)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	wallet.SetProvider(c.Provider)
   102  
   103  	err = c.FundAddress(wallet.Address())
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	return wallet, nil
   109  }
   110  
   111  func (c *Testchain) MustWallet(optAccountIndex ...uint32) *ethwallet.Wallet {
   112  	wallet, err := c.Wallet()
   113  	if err != nil {
   114  		panic(err)
   115  	}
   116  	if len(optAccountIndex) > 0 {
   117  		_, err = wallet.SelfDeriveAccountIndex(optAccountIndex[0])
   118  		if err != nil {
   119  			panic(err)
   120  		}
   121  	}
   122  
   123  	err = c.FundAddress(wallet.Address())
   124  	if err != nil {
   125  		panic(err)
   126  	}
   127  
   128  	return wallet
   129  }
   130  
   131  func (c *Testchain) DummyWallet(seed uint64) (*ethwallet.Wallet, error) {
   132  	wallet, err := ethwallet.NewWalletFromPrivateKey(DummyPrivateKey(seed))
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	wallet.SetProvider(c.Provider)
   137  	return wallet, nil
   138  }
   139  
   140  func (c *Testchain) DummyWallets(nWallets uint64, startingSeed uint64) ([]*ethwallet.Wallet, error) {
   141  	var wallets []*ethwallet.Wallet
   142  
   143  	for i := uint64(0); i < nWallets; i++ {
   144  		wallet, err := c.DummyWallet(startingSeed + i*1000)
   145  		if err != nil {
   146  			return nil, err
   147  		}
   148  		wallets = append(wallets, wallet)
   149  	}
   150  
   151  	return wallets, nil
   152  }
   153  
   154  func (c *Testchain) FundWallets(minBalance float64, wallets ...*ethwallet.Wallet) error {
   155  	minTarget := ETHValue(minBalance)
   156  	fundAddresses := []ethkit.Address{}
   157  
   158  	for _, wallet := range wallets {
   159  		balance, err := c.Provider.BalanceAt(context.Background(), wallet.Address(), nil)
   160  		if err != nil {
   161  			return err
   162  		}
   163  		if balance.Cmp(minTarget) < 0 {
   164  			fundAddresses = append(fundAddresses, wallet.Address())
   165  		}
   166  	}
   167  
   168  	return c.FundAddresses(fundAddresses, minBalance)
   169  }
   170  
   171  func (c *Testchain) FundAddress(addr common.Address, optBalanceTarget ...float64) error {
   172  	target := ETHValue(100)
   173  	if len(optBalanceTarget) > 0 {
   174  		target = ETHValue(optBalanceTarget[0])
   175  	}
   176  
   177  	balance, err := c.Provider.BalanceAt(context.Background(), addr, nil)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	if balance.Cmp(target) >= 0 {
   183  		// skip, we have enough funds in this wallet for the target
   184  		return nil
   185  	}
   186  
   187  	var accounts []common.Address
   188  	call := ethrpc.NewCallBuilder[[]common.Address]("eth_accounts", nil)
   189  	_, err = c.Provider.Do(context.Background(), call.Into(&accounts))
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	type SendTx struct {
   195  		From  *common.Address `json:"from"`
   196  		To    *common.Address `json:"to"`
   197  		Value string          `json:"value"`
   198  	}
   199  
   200  	amount := big.NewInt(0)
   201  	amount.Sub(target, balance)
   202  	// if balance.Cmp(target) < 0 {
   203  	// 	// top up to the target
   204  	// 	amount.Sub(target, balance)
   205  	// } else {
   206  	// 	// already at the target, add same target quantity
   207  	// 	amount.Set(target)
   208  	// }
   209  
   210  	tx := &SendTx{
   211  		From:  &accounts[0],
   212  		To:    &addr,
   213  		Value: "0x" + amount.Text(16),
   214  	}
   215  
   216  	_, err = c.Provider.Do(context.Background(), ethrpc.NewCall("eth_sendTransaction", tx))
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	for i := 0; i < 10; i++ {
   222  		time.Sleep(1 * time.Second)
   223  		balance, err = c.Provider.BalanceAt(context.Background(), addr, nil)
   224  		if err != nil {
   225  			return err
   226  		}
   227  		if balance.Cmp(target) >= 0 {
   228  			return nil
   229  		}
   230  	}
   231  
   232  	return fmt.Errorf("test wallet failed to fund")
   233  }
   234  
   235  func (c *Testchain) MustFundAddress(addr common.Address, optBalanceTarget ...float64) {
   236  	err := c.FundAddress(addr, optBalanceTarget...)
   237  	if err != nil {
   238  		panic(err)
   239  	}
   240  }
   241  
   242  func (c *Testchain) FundAddresses(addrs []common.Address, optBalanceTarget ...float64) error {
   243  	for _, addr := range addrs {
   244  		err := c.FundAddress(addr)
   245  		if err != nil {
   246  			return err
   247  		}
   248  	}
   249  	return nil
   250  }
   251  
   252  func (c *Testchain) GetDeployWallet() *ethwallet.Wallet {
   253  	return c.MustWallet(5)
   254  }
   255  
   256  // GetDeployTransactor returns a account transactor typically used for deploying contracts
   257  func (c *Testchain) GetDeployTransactor() (*bind.TransactOpts, error) {
   258  	return c.GetDeployWallet().Transactor(context.Background())
   259  }
   260  
   261  // GetRelayerWallet is the wallet dedicated EOA wallet to relaying transactions
   262  func (c *Testchain) GetRelayerWallet() *ethwallet.Wallet {
   263  	return c.MustWallet(6)
   264  }
   265  
   266  // Deploy will deploy a contract registered in `Contracts` registry using the standard deployment method. Each Deploy call
   267  // will instanitate a new contract on the test chain.
   268  func (c *Testchain) Deploy(t *testing.T, contractName string, contractConstructorArgs ...interface{}) (*ethcontract.Contract, *types.Receipt) {
   269  	artifact, ok := Contracts.Get(contractName)
   270  	if !ok {
   271  		t.Fatal(fmt.Errorf("contract abi not found for name %s", contractName))
   272  	}
   273  
   274  	data := make([]byte, len(artifact.Bin))
   275  	copy(data, artifact.Bin)
   276  
   277  	var input []byte
   278  	var err error
   279  
   280  	// encode constructor call
   281  	if len(contractConstructorArgs) > 0 && len(artifact.ABI.Constructor.Inputs) > 0 {
   282  		input, err = artifact.ABI.Pack("", contractConstructorArgs...)
   283  	} else {
   284  		input, err = artifact.ABI.Pack("")
   285  	}
   286  	if err != nil {
   287  		t.Fatal(fmt.Errorf("contract constructor pack failed: %w", err))
   288  	}
   289  
   290  	// append constructor calldata at end of the contract bin
   291  	data = append(data, input...)
   292  
   293  	wallet := c.GetDeployWallet()
   294  	signedTxn, err := wallet.NewTransaction(context.Background(), &ethtxn.TransactionRequest{
   295  		Data: data,
   296  	})
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	_, waitTx, err := wallet.SendTransaction(context.Background(), signedTxn)
   301  	if err != nil {
   302  		t.Fatal(err)
   303  	}
   304  	receipt, err := waitTx(context.Background())
   305  	if err != nil {
   306  		t.Fatal(err)
   307  	}
   308  	if receipt.Status != types.ReceiptStatusSuccessful {
   309  		t.Fatal(fmt.Errorf("txn failed: %w", err))
   310  	}
   311  
   312  	return ethcontract.NewContractCaller(receipt.ContractAddress, artifact.ABI, c.Provider), receipt
   313  }
   314  
   315  func (c *Testchain) WaitMined(txn common.Hash) error {
   316  	_, err := ethrpc.WaitForTxnReceipt(context.Background(), c.Provider, txn)
   317  	return err
   318  }
   319  
   320  func (c *Testchain) RandomNonce() *big.Int {
   321  	space := big.NewInt(int64(time.Now().Nanosecond()))
   322  	return space
   323  }
   324  
   325  // parseTestWalletMnemonic parses the wallet mnemonic from ./package.json, the same
   326  // key used to start the test chain server.
   327  func parseTestWalletMnemonic() (string, error) {
   328  	_, filename, _, _ := runtime.Caller(0)
   329  	cwd := filepath.Dir(filename)
   330  
   331  	packageJSONFile := filepath.Join(cwd, "./testchain/package.json")
   332  	data, err := os.ReadFile(packageJSONFile)
   333  	if err != nil {
   334  		return "", fmt.Errorf("ParseTestWalletMnemonic, read: %w", err)
   335  	}
   336  
   337  	var dict struct {
   338  		Config struct {
   339  			Mnemonic string `json:"mnemonic"`
   340  		} `json:"config"`
   341  	}
   342  	err = json.Unmarshal(data, &dict)
   343  	if err != nil {
   344  		return "", fmt.Errorf("ParseTestWalletMnemonic, unmarshal: %w", err)
   345  	}
   346  
   347  	return dict.Config.Mnemonic, nil
   348  }