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(), ðtxn.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 }