From 7cff7bec23b66bebc3903d0172e25f79d296d27d Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Mon, 26 Jun 2023 15:03:51 -0700 Subject: [PATCH] test(e2e): support running against live networks (#1630) * add NodeRunner impl for connecting to live network * refactor out node runner setups * remove hardcoded denom for DeployedErc20 * further specify restrictions on DeployedErc20 * don't override .env funded account mnemonic * lower amounts for convert to coin e2e tests * lower fund values used by e2e tests * add doc comments for all e2e functions & types --- Makefile | 1 - tests/e2e/.env | 4 +- tests/e2e/.env.live-network-example | 20 +++++ tests/e2e/e2e_convert_cosmos_coins_test.go | 16 ++-- tests/e2e/e2e_evm_contracts_test.go | 26 +++--- tests/e2e/e2e_min_fees_test.go | 4 +- tests/e2e/e2e_test.go | 10 +-- tests/e2e/runner/chain.go | 10 +++ tests/e2e/runner/kvtool.go | 7 ++ tests/e2e/runner/live.go | 80 ++++++++++++++++++ tests/e2e/runner/main.go | 8 +- tests/e2e/testutil/account.go | 4 + tests/e2e/testutil/chain.go | 1 + tests/e2e/testutil/config.go | 31 +++++++ tests/e2e/testutil/eip712.go | 2 + tests/e2e/testutil/init_evm.go | 33 +++++++- tests/e2e/testutil/suite.go | 95 +++++++++++++++++----- 17 files changed, 298 insertions(+), 54 deletions(-) create mode 100644 tests/e2e/.env.live-network-example create mode 100644 tests/e2e/runner/live.go diff --git a/Makefile b/Makefile index f2590f07..844799dc 100644 --- a/Makefile +++ b/Makefile @@ -294,7 +294,6 @@ test-basic: test # run end-to-end tests (local docker container must be built, see docker-build) test-e2e: docker-build - export E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC='tent fitness boat among census primary pipe nose dream glance cave turtle electric fabric jacket shaft easy myself genuine this sibling pulse word unfold'; \ go test -failfast -count=1 -v ./tests/e2e/... test: diff --git a/tests/e2e/.env b/tests/e2e/.env index f3ddfb59..9c8ec26a 100644 --- a/tests/e2e/.env +++ b/tests/e2e/.env @@ -29,5 +29,7 @@ E2E_KAVA_UPGRADE_HEIGHT= E2E_KAVA_UPGRADE_BASE_IMAGE_TAG= # E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token. -# The E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC account should have a balance. +# The E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC account must have a balance. +# The ERC20 must be enabled via x/evmutil params for conversion to sdk.Coin. +# The corresponding sdk.Coin must be a supported vault in x/earn. E2E_KAVA_ERC20_ADDRESS=0xeA7100edA2f805356291B0E55DaD448599a72C6d diff --git a/tests/e2e/.env.live-network-example b/tests/e2e/.env.live-network-example new file mode 100644 index 00000000..446488bb --- /dev/null +++ b/tests/e2e/.env.live-network-example @@ -0,0 +1,20 @@ +# E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC is for a funded account used to intialize all new testing accounts. +# Should be funded with KAVA and have an ERC20 balance +E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC='tent fitness boat among census primary pipe nose dream glance cave turtle electric fabric jacket shaft easy myself genuine this sibling pulse word unfold' + +# E2E_RUN_KVTOOL_NETWORKS must be false to trigger run on live network +E2E_RUN_KVTOOL_NETWORKS=false + +# Configure the endpoints for connecting to the running network here. +E2E_KAVA_RPC_URL='http://localhost:26657' +E2E_KAVA_GRPC_URL='http://localhost:9090' +E2E_KAVA_EMV_RPC_URL='http://localhost:8545' + +# E2E_INCLUDE_IBC_TESTS is not currently supported for running tests against a live network. +E2E_INCLUDE_IBC_TESTS=false + +# E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token. +# The E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC account must have a balance. +# The ERC20 must be enabled via x/evmutil params for conversion to sdk.Coin. +# The corresponding sdk.Coin must be a supported vault in x/earn. +E2E_KAVA_ERC20_ADDRESS=0xeA7100edA2f805356291B0E55DaD448599a72C6d diff --git a/tests/e2e/e2e_convert_cosmos_coins_test.go b/tests/e2e/e2e_convert_cosmos_coins_test.go index 1c9d24a4..7ea21520 100644 --- a/tests/e2e/e2e_convert_cosmos_coins_test.go +++ b/tests/e2e/e2e_convert_cosmos_coins_test.go @@ -32,7 +32,7 @@ func setupConvertToCoinTest( denom = tokenInfo.CosmosDenom initialFunds = sdk.NewCoins( sdk.NewInt64Coin(suite.Kava.StakingDenom, 1e6), // gas money - sdk.NewInt64Coin(denom, 1e10), // conversion-enabled cosmos coin + sdk.NewInt64Coin(denom, 1e6), // conversion-enabled cosmos coin ) user = suite.Kava.NewFundedAccount(accountName, initialFunds) @@ -40,12 +40,12 @@ func setupConvertToCoinTest( return denom, initialFunds, user } -// amount must be less than 1e10 +// amount must be less than initial funds (1e6) func (suite *IntegrationTestSuite) setupAccountWithCosmosCoinERC20Balance( accountName string, amount int64, ) (user *testutil.SigningAccount, contractAddress *evmutiltypes.InternalEVMAddress, denom string, sdkBalance sdk.Coins) { - if amount > 1e10 { - panic("test erc20 amount must be less than 1e10") + if amount > 1e6 { + panic("test erc20 amount must be less than 1e6") } denom, sdkBalance, user = setupConvertToCoinTest(suite, accountName) @@ -88,7 +88,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter") fee := sdk.NewCoins(ukava(7500)) - convertAmount := int64(5e9) + convertAmount := int64(5e5) initialModuleBalance := suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) /////////////////////////////// @@ -168,7 +168,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter-eip712") - convertAmount := int64(5e9) + convertAmount := int64(5e5) initialModuleBalance := suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) /////////////////////////////// @@ -325,14 +325,14 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ForbiddenERC20Calls() // - check complex conversion flow. bob converts funds they receive on evm back to sdk.Coin func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ERC20Magic() { fee := sdk.NewCoins(ukava(7500)) - initialAliceAmount := int64(2e6) + initialAliceAmount := int64(2e5) alice, contractAddress, denom, _ := suite.setupAccountWithCosmosCoinERC20Balance( "cosmo-coin-converter-complex-alice", initialAliceAmount, ) gasMoney := sdk.NewCoins(ukava(1e6)) bob := suite.Kava.NewFundedAccount("cosmo-coin-converter-complex-bob", gasMoney) - amount := big.NewInt(1e6) + amount := big.NewInt(1e5) // bob can't move alice's funds nonce, err := bob.NextNonce() diff --git a/tests/e2e/e2e_evm_contracts_test.go b/tests/e2e/e2e_evm_contracts_test.go index 422cea79..174a94f5 100644 --- a/tests/e2e/e2e_evm_contracts_test.go +++ b/tests/e2e/e2e_evm_contracts_test.go @@ -22,7 +22,7 @@ func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() { // this test manipulates state of the Greeter contract which means other tests shouldn't use it. // setup funded account to interact with contract - user := suite.Kava.NewFundedAccount("greeter-contract-user", sdk.NewCoins(ukava(10e6))) + user := suite.Kava.NewFundedAccount("greeter-contract-user", sdk.NewCoins(ukava(1e6))) greeterAddr := suite.Kava.ContractAddrs["greeter"] contract, err := greeter.NewGreeter(greeterAddr, suite.Kava.EvmClient) @@ -47,17 +47,17 @@ func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() { func (suite *IntegrationTestSuite) TestEthCallToErc20() { randoReceiver := util.SdkToEvmAddress(app.RandomAddress()) - amount := big.NewInt(1e6) + amount := big.NewInt(1e3) // make unauthenticated eth_call query to check balance - beforeBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, randoReceiver) + beforeBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20.Address, randoReceiver) // make authenticate eth_call to transfer tokens res := suite.FundKavaErc20Balance(randoReceiver, amount) suite.NoError(res.Err) // make another unauthenticated eth_call query to check new balance - afterBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, randoReceiver) + afterBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20.Address, randoReceiver) suite.BigIntsEqual(big.NewInt(0), beforeBalance, "expected before balance to be zero") suite.BigIntsEqual(amount, afterBalance, "unexpected post-transfer balance") @@ -65,12 +65,12 @@ func (suite *IntegrationTestSuite) TestEthCallToErc20() { func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() { // create new funded account - sender := suite.Kava.NewFundedAccount("eip712-msgSend", sdk.NewCoins(ukava(10e6))) + sender := suite.Kava.NewFundedAccount("eip712-msgSend", sdk.NewCoins(ukava(2e4))) receiver := app.RandomAddress() - // setup message for sending 1KAVA to random receiver + // setup message for sending some kava to random receiver msgs := []sdk.Msg{ - banktypes.NewMsgSend(sender.SdkAddress, receiver, sdk.NewCoins(ukava(1e6))), + banktypes.NewMsgSend(sender.SdkAddress, receiver, sdk.NewCoins(ukava(1e3))), } // create tx @@ -103,16 +103,16 @@ func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() { Denom: "ukava", }) suite.NoError(err) - suite.Equal(sdk.NewInt(1e6), balRes.Balance.Amount) + suite.Equal(sdk.NewInt(1e3), balRes.Balance.Amount) } // Note that this test works because the deployed erc20 is configured in evmutil & earn params. func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() { - amount := sdk.NewInt(10e6) // 10 USDC - sdkDenom := "erc20/multichain/usdc" + amount := sdk.NewInt(1e4) // .04 USDC + sdkDenom := suite.DeployedErc20.CosmosDenom // create new funded account - depositor := suite.Kava.NewFundedAccount("eip712-earn-depositor", sdk.NewCoins(ukava(1e6))) + depositor := suite.Kava.NewFundedAccount("eip712-earn-depositor", sdk.NewCoins(ukava(1e5))) // give them erc20 balance to deposit fundRes := suite.FundKavaErc20Balance(depositor.EvmAddress, amount.BigInt()) suite.NoError(fundRes.Err) @@ -121,7 +121,7 @@ func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() { convertMsg := evmutiltypes.NewMsgConvertERC20ToCoin( evmutiltypes.NewInternalEVMAddress(depositor.EvmAddress), depositor.SdkAddress, - evmutiltypes.NewInternalEVMAddress(suite.DeployedErc20Address), + evmutiltypes.NewInternalEVMAddress(suite.DeployedErc20.Address), amount, ) depositMsg := earntypes.NewMsgDeposit( @@ -161,7 +161,7 @@ func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() { suite.NoError(err) // check that depositor no longer has erc20 balance - balance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, depositor.EvmAddress) + balance := suite.Kava.GetErc20Balance(suite.DeployedErc20.Address, depositor.EvmAddress) suite.BigIntsEqual(big.NewInt(0), balance, "expected no erc20 balance") // check that account has an earn deposit position diff --git a/tests/e2e/e2e_min_fees_test.go b/tests/e2e/e2e_min_fees_test.go index 882d6b15..cfde3836 100644 --- a/tests/e2e/e2e_min_fees_test.go +++ b/tests/e2e/e2e_min_fees_test.go @@ -33,7 +33,7 @@ func (suite *IntegrationTestSuite) TestEthGasPriceReturnsMinFee() { func (suite *IntegrationTestSuite) TestEvmRespectsMinFee() { // setup sender & receiver - sender := suite.Kava.NewFundedAccount("evm-min-fee-test-sender", sdk.NewCoins(ukava(2e6))) + sender := suite.Kava.NewFundedAccount("evm-min-fee-test-sender", sdk.NewCoins(ukava(1e3))) randoReceiver := util.SdkToEvmAddress(app.RandomAddress()) // get min gas price for evm (from app.toml) @@ -44,7 +44,7 @@ func (suite *IntegrationTestSuite) TestEvmRespectsMinFee() { // attempt tx with less than min gas price (min fee - 1) tooLowGasPrice := minGasPrice.Sub(sdk.OneInt()).BigInt() req := util.EvmTxRequest{ - Tx: ethtypes.NewTransaction(0, randoReceiver, big.NewInt(1e6), 1e5, tooLowGasPrice, nil), + Tx: ethtypes.NewTransaction(0, randoReceiver, big.NewInt(5e2), 1e5, tooLowGasPrice, nil), Data: "this tx should fail because it's gas price is too low", } res := sender.SignAndBroadcastEvmTx(req) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index c89de572..8bb15b00 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -80,7 +80,7 @@ func (suite *IntegrationTestSuite) TestFundedAccount() { // example test that signs & broadcasts an EVM tx func (suite *IntegrationTestSuite) TestTransferOverEVM() { // fund an account that can perform the transfer - initialFunds := ukava(1e7) // 10 KAVA + initialFunds := ukava(1e6) // 1 KAVA acc := suite.Kava.NewFundedAccount("evm-test-transfer", sdk.NewCoins(initialFunds)) // get a rando account to send kava to @@ -93,7 +93,7 @@ func (suite *IntegrationTestSuite) TestTransferOverEVM() { suite.Equal(uint64(0), nonce) // sanity check. the account should have no prior txs // transfer kava over EVM - kavaToTransfer := big.NewInt(1e18) // 1 KAVA; akava has 18 decimals. + kavaToTransfer := big.NewInt(1e17) // .1 KAVA; akava has 18 decimals. req := util.EvmTxRequest{ Tx: ethtypes.NewTransaction(nonce, to, kavaToTransfer, 1e5, minEvmGasPrice, nil), Data: "any ol' data to track this through the system", @@ -109,7 +109,7 @@ func (suite *IntegrationTestSuite) TestTransferOverEVM() { // expect (9 - gas used) KAVA remaining in account. balance := suite.Kava.QuerySdkForBalances(acc.SdkAddress) - suite.Equal(sdkmath.NewInt(9e6).Sub(ukavaUsedForGas), balance.AmountOf("ukava")) + suite.Equal(sdkmath.NewInt(9e5).Sub(ukavaUsedForGas), balance.AmountOf("ukava")) } // TestIbcTransfer transfers KAVA from the primary kava chain (suite.Kava) to the ibc chain (suite.Ibc). @@ -119,7 +119,7 @@ func (suite *IntegrationTestSuite) TestIbcTransfer() { // ARRANGE // setup kava account - funds := ukava(1e7) // 10 KAVA + funds := ukava(1e5) // .1 KAVA kavaAcc := suite.Kava.NewFundedAccount("ibc-transfer-kava-side", sdk.NewCoins(funds)) // setup ibc account ibcAcc := suite.Ibc.NewFundedAccount("ibc-transfer-ibc-side", sdk.NewCoins()) @@ -127,7 +127,7 @@ func (suite *IntegrationTestSuite) TestIbcTransfer() { gasLimit := int64(2e5) fee := ukava(7500) - fundsToSend := ukava(5e6) // 5 KAVA + fundsToSend := ukava(5e4) // .005 KAVA transferMsg := ibctypes.NewMsgTransfer( testutil.IbcPort, testutil.IbcChannel, diff --git a/tests/e2e/runner/chain.go b/tests/e2e/runner/chain.go index f4a3efa1..2dcffa16 100644 --- a/tests/e2e/runner/chain.go +++ b/tests/e2e/runner/chain.go @@ -24,22 +24,30 @@ type ChainDetails struct { StakingDenom string } +// EvmClient dials the underlying EVM RPC url and returns an ethclient. func (c ChainDetails) EvmClient() (*ethclient.Client, error) { return ethclient.Dial(c.EvmRpcUrl) } +// GrpcConn creates a new connection to the underlying Grpc url. func (c ChainDetails) GrpcConn() (*grpc.ClientConn, error) { return util.NewGrpcConnection(c.GrpcUrl) } +// Chains wraps a map of name -> details about how to connect to a chain. +// It prevents registering multiple chains with the same name & encapsulates +// panicking if attempting to access a chain that does not exist. type Chains struct { byName map[string]*ChainDetails } +// NewChains creates an empty Chains map. func NewChains() Chains { return Chains{byName: make(map[string]*ChainDetails, 0)} } +// MustGetChain returns the chain of a given name, +// or panics if a chain with that name has not been registered. func (c Chains) MustGetChain(name string) *ChainDetails { chain, found := c.byName[name] if !found { @@ -48,6 +56,8 @@ func (c Chains) MustGetChain(name string) *ChainDetails { return chain } +// Register adds a chain to the map. +// It returns an error if a ChainDetails with that name has already been registered. func (c *Chains) Register(name string, chain *ChainDetails) error { if _, found := c.byName[name]; found { return ErrChainAlreadyExists diff --git a/tests/e2e/runner/kvtool.go b/tests/e2e/runner/kvtool.go index 93e5ee21..087e7527 100644 --- a/tests/e2e/runner/kvtool.go +++ b/tests/e2e/runner/kvtool.go @@ -32,12 +32,15 @@ type KvtoolRunner struct { var _ NodeRunner = &KvtoolRunner{} +// NewKvtoolRunner creates a new KvtoolRunner. func NewKvtoolRunner(config KvtoolRunnerConfig) *KvtoolRunner { return &KvtoolRunner{ config: config, } } +// StartChains implements NodeRunner. +// For KvtoolRunner, it sets up, runs, and connects to a local chain via kvtool. func (k *KvtoolRunner) StartChains() Chains { // install kvtool if not already installed installKvtoolCmd := exec.Command("./scripts/install-kvtool.sh") @@ -89,6 +92,10 @@ func (k *KvtoolRunner) StartChains() Chains { return chains } +// Shutdown implements NodeRunner. +// For KvtoolRunner, it shuts down the local kvtool network. +// To prevent shutting down the chain (eg. to preserve logs or examine post-test state) +// use the `SkipShutdown` option on the config. func (k *KvtoolRunner) Shutdown() { if k.config.SkipShutdown { log.Printf("would shut down but SkipShutdown is true") diff --git a/tests/e2e/runner/live.go b/tests/e2e/runner/live.go new file mode 100644 index 00000000..317deb58 --- /dev/null +++ b/tests/e2e/runner/live.go @@ -0,0 +1,80 @@ +package runner + +import ( + "context" + "fmt" + + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// LiveNodeRunnerConfig implements NodeRunner. +// It connects to a running network via the RPC, GRPC, and EVM urls. +type LiveNodeRunnerConfig struct { + KavaRpcUrl string + KavaGrpcUrl string + KavaEvmRpcUrl string +} + +// LiveNodeRunner implements NodeRunner for an already-running chain. +// If a LiveNodeRunner is used, end-to-end tests are run against a live chain. +type LiveNodeRunner struct { + config LiveNodeRunnerConfig +} + +var _ NodeRunner = LiveNodeRunner{} + +// NewLiveNodeRunner creates a new LiveNodeRunner. +func NewLiveNodeRunner(config LiveNodeRunnerConfig) *LiveNodeRunner { + return &LiveNodeRunner{config} +} + +// StartChains implements NodeRunner. +// It initializes connections to the chain based on parameters. +// It attempts to ping the necessary endpoints and panics if they cannot be reached. +func (r LiveNodeRunner) StartChains() Chains { + fmt.Println("establishing connection to live kava network") + chains := NewChains() + + kavaChain := ChainDetails{ + RpcUrl: r.config.KavaRpcUrl, + GrpcUrl: r.config.KavaGrpcUrl, + EvmRpcUrl: r.config.KavaEvmRpcUrl, + } + + if err := waitForChainStart(kavaChain); err != nil { + panic(fmt.Sprintf("failed to ping chain: %s", err)) + } + + // determine chain id + grpc, err := kavaChain.GrpcConn() + if err != nil { + panic(fmt.Sprintf("failed to establish grpc conn to %s: %s", r.config.KavaGrpcUrl, err)) + } + tm := tmservice.NewServiceClient(grpc) + nodeInfo, err := tm.GetNodeInfo(context.Background(), &tmservice.GetNodeInfoRequest{}) + if err != nil { + panic(fmt.Sprintf("failed to fetch kava node info: %s", err)) + } + kavaChain.ChainId = nodeInfo.DefaultNodeInfo.Network + + // determine staking denom + staking := stakingtypes.NewQueryClient(grpc) + stakingParams, err := staking.Params(context.Background(), &stakingtypes.QueryParamsRequest{}) + if err != nil { + panic(fmt.Sprintf("failed to fetch kava staking params: %s", err)) + } + kavaChain.StakingDenom = stakingParams.Params.BondDenom + + chains.Register("kava", &kavaChain) + + fmt.Printf("successfully connected to live network %+v\n", kavaChain) + + return chains +} + +// Shutdown implements NodeRunner. +// As the chains are externally operated, this is a no-op. +func (LiveNodeRunner) Shutdown() { + fmt.Println("shutting down e2e test connections.") +} diff --git a/tests/e2e/runner/main.go b/tests/e2e/runner/main.go index b2ff8677..ddb93192 100644 --- a/tests/e2e/runner/main.go +++ b/tests/e2e/runner/main.go @@ -15,26 +15,28 @@ type NodeRunner interface { Shutdown() } +// waitForChainStart sets a timeout and repeatedly pings the chains. +// If the chain is successfully reached before the timeout, this returns no error. func waitForChainStart(chainDetails ChainDetails) error { // exponential backoff on trying to ping the node, timeout after 30 seconds b := backoff.NewExponentialBackOff() b.MaxInterval = 5 * time.Second b.MaxElapsedTime = 30 * time.Second if err := backoff.Retry(func() error { return pingKava(chainDetails.RpcUrl) }, b); err != nil { - return fmt.Errorf("failed to start & connect to chain: %s", err) + return fmt.Errorf("failed connect to chain: %s", err) } b.Reset() // the evm takes a bit longer to start up. wait for it to start as well. if err := backoff.Retry(func() error { return pingEvm(chainDetails.EvmRpcUrl) }, b); err != nil { - return fmt.Errorf("failed to start & connect to chain: %s", err) + return fmt.Errorf("failed connect to chain: %s", err) } return nil } func pingKava(rpcUrl string) error { - log.Println("pinging kava chain...") statusUrl := fmt.Sprintf("%s/status", rpcUrl) + log.Printf("pinging kava chain: %s\n", statusUrl) res, err := http.Get(statusUrl) if err != nil { return err diff --git a/tests/e2e/testutil/account.go b/tests/e2e/testutil/account.go index 11b7afa2..085cbde4 100644 --- a/tests/e2e/testutil/account.go +++ b/tests/e2e/testutil/account.go @@ -27,6 +27,8 @@ import ( "github.com/kava-labs/kava/tests/util" ) +// SigningAccount wraps details about an account and its private keys. +// It exposes functionality for signing and broadcasting transactions. type SigningAccount struct { name string mnemonic string @@ -173,6 +175,8 @@ func (a *SigningAccount) SignAndBroadcastEvmTx(req util.EvmTxRequest) EvmTxRespo return response } +// SignRawEvmData signs raw evm data with the SigningAccount's private key. +// It does not broadcast the signed data. func (a *SigningAccount) SignRawEvmData(msg []byte) ([]byte, types.PubKey, error) { keyringSigner := emtests.NewSigner(a.evmPrivKey) return keyringSigner.SignByAddress(a.SdkAddress, msg) diff --git a/tests/e2e/testutil/chain.go b/tests/e2e/testutil/chain.go index cf3b3f8d..160eb3f3 100644 --- a/tests/e2e/testutil/chain.go +++ b/tests/e2e/testutil/chain.go @@ -133,6 +133,7 @@ func (chain *Chain) GetModuleBalances(moduleName string) sdk.Coins { return chain.QuerySdkForBalances(addr) } +// GetErc20Balance fetches the ERC20 balance of `contract` for `address`. func (chain *Chain) GetErc20Balance(contract, address common.Address) *big.Int { resData, err := chain.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ To: &contract, diff --git a/tests/e2e/testutil/config.go b/tests/e2e/testutil/config.go index 10befaeb..367e8fb4 100644 --- a/tests/e2e/testutil/config.go +++ b/tests/e2e/testutil/config.go @@ -13,12 +13,15 @@ func init() { gotenv.Load() } +// SuiteConfig wraps configuration details for running the end-to-end test suite. type SuiteConfig struct { // A funded account used to fnd all other accounts. FundedAccountMnemonic string // A config for using kvtool local networks for the test run Kvtool *KvtoolConfig + // A config for connecting to a running network + LiveNetwork *LiveNetworkConfig // Whether or not to start an IBC chain. Use `suite.SkipIfIbcDisabled()` in IBC tests in IBC tests. IncludeIbcTests bool @@ -30,6 +33,8 @@ type SuiteConfig struct { SkipShutdown bool } +// KvtoolConfig wraps configuration options for running the end-to-end test suite against +// a locally running chain. This config must be defined if E2E_RUN_KVTOOL_NETWORKS is true. type KvtoolConfig struct { // The kava.configTemplate flag to be passed to kvtool, usually "master". // This allows one to change the base genesis used to start the chain. @@ -45,6 +50,15 @@ type KvtoolConfig struct { KavaUpgradeBaseImageTag string } +// LiveNetworkConfig wraps configuration options for running the end-to-end test suite +// against a live network. It must be defined if E2E_RUN_KVTOOL_NETWORKS is false. +type LiveNetworkConfig struct { + KavaRpcUrl string + KavaGrpcUrl string + KavaEvmRpcUrl string +} + +// ParseSuiteConfig builds a SuiteConfig from environment variables. func ParseSuiteConfig() SuiteConfig { config := SuiteConfig{ // this mnemonic is expected to be a funded account that can seed the funds for all @@ -63,11 +77,15 @@ func ParseSuiteConfig() SuiteConfig { if useKvtoolNetworks { kvtoolConfig := ParseKvtoolConfig() config.Kvtool = &kvtoolConfig + } else { + liveNetworkConfig := ParseLiveNetworkConfig() + config.LiveNetwork = &liveNetworkConfig } return config } +// ParseKvtoolConfig builds a KvtoolConfig from environment variables. func ParseKvtoolConfig() KvtoolConfig { config := KvtoolConfig{ KavaConfigTemplate: nonemptyStringEnv("E2E_KVTOOL_KAVA_CONFIG_TEMPLATE"), @@ -87,6 +105,17 @@ func ParseKvtoolConfig() KvtoolConfig { return config } +// ParseLiveNetworkConfig builds a LiveNetworkConfig from environment variables. +func ParseLiveNetworkConfig() LiveNetworkConfig { + return LiveNetworkConfig{ + KavaRpcUrl: nonemptyStringEnv("E2E_KAVA_RPC_URL"), + KavaGrpcUrl: nonemptyStringEnv("E2E_KAVA_GRPC_URL"), + KavaEvmRpcUrl: nonemptyStringEnv("E2E_KAVA_EMV_RPC_URL"), + } +} + +// mustParseBool is a helper method that panics if the env variable `name` +// cannot be parsed to a boolean func mustParseBool(name string) bool { envValue := os.Getenv(name) if envValue == "" { @@ -99,6 +128,8 @@ func mustParseBool(name string) bool { return value } +// nonemptyStringEnv is a helper method that panics if the env variable `name` +// is empty or undefined. func nonemptyStringEnv(name string) string { value := os.Getenv(name) if value == "" { diff --git a/tests/e2e/testutil/eip712.go b/tests/e2e/testutil/eip712.go index 1dd8f128..7cc45d46 100644 --- a/tests/e2e/testutil/eip712.go +++ b/tests/e2e/testutil/eip712.go @@ -16,6 +16,8 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" ) +// NewEip712TxBuilder is a helper method for creating an EIP712 signed tx +// A tx like this is what a user signing cosmos messages with Metamask would broadcast. func (suite *E2eTestSuite) NewEip712TxBuilder( acc *SigningAccount, chain *Chain, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg, memo string, ) client.TxBuilder { diff --git a/tests/e2e/testutil/init_evm.go b/tests/e2e/testutil/init_evm.go index 6106ad53..85abd00b 100644 --- a/tests/e2e/testutil/init_evm.go +++ b/tests/e2e/testutil/init_evm.go @@ -10,6 +10,8 @@ import ( "github.com/kava-labs/kava/tests/e2e/contracts/greeter" "github.com/kava-labs/kava/tests/util" + "github.com/kava-labs/kava/x/earn/types" + evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" ) // InitKavaEvmData is run after the chain is running, but before the tests are run. @@ -18,11 +20,37 @@ func (suite *E2eTestSuite) InitKavaEvmData() { whale := suite.Kava.GetAccount(FundedAccountName) // ensure funded account has nonzero erc20 balance - balance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, whale.EvmAddress) + balance := suite.Kava.GetErc20Balance(suite.DeployedErc20.Address, whale.EvmAddress) if balance.Cmp(big.NewInt(0)) != 1 { panic(fmt.Sprintf("expected funded account (%s) to have erc20 balance", whale.EvmAddress.Hex())) } + // expect the erc20 to be enabled for conversion to sdk.Coin + params, err := suite.Kava.Evmutil.Params(context.Background(), &evmutiltypes.QueryParamsRequest{}) + if err != nil { + panic(fmt.Sprintf("failed to fetch evmutil params during init: %s", err)) + } + found := false + erc20Addr := suite.DeployedErc20.Address.Hex() + for _, p := range params.Params.EnabledConversionPairs { + if common.BytesToAddress(p.KavaERC20Address).Hex() == erc20Addr { + found = true + suite.DeployedErc20.CosmosDenom = p.Denom + } + } + if !found { + panic(fmt.Sprintf("erc20 %s must be enabled for conversion to cosmos coin", erc20Addr)) + } + + // expect the erc20's cosmos denom to be a supported earn vault + _, err = suite.Kava.Earn.Vault( + context.Background(), + types.NewQueryVaultRequest(suite.DeployedErc20.CosmosDenom), + ) + if err != nil { + panic(fmt.Sprintf("failed to find earn vault with denom %s: %s", suite.DeployedErc20.CosmosDenom, err)) + } + // deploy an example contract greeterAddr, _, _, err := greeter.DeployGreeter( whale.evmSigner.Auth, @@ -33,6 +61,7 @@ func (suite *E2eTestSuite) InitKavaEvmData() { suite.Kava.ContractAddrs["greeter"] = greeterAddr } +// FundKavaErc20Balance sends the pre-deployed ERC20 token to the `toAddress`. func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount *big.Int) EvmTxResponse { // funded account should have erc20 balance whale := suite.Kava.GetAccount(FundedAccountName) @@ -42,7 +71,7 @@ func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount suite.NoError(err) req := util.EvmTxRequest{ - Tx: ethtypes.NewTransaction(nonce, suite.DeployedErc20Address, big.NewInt(0), 1e5, big.NewInt(1e10), data), + Tx: ethtypes.NewTransaction(nonce, suite.DeployedErc20.Address, big.NewInt(0), 1e5, big.NewInt(1e10), data), Data: fmt.Sprintf("fund %s with ERC20 balance (%s)", toAddress.Hex(), amount.String()), } diff --git a/tests/e2e/testutil/suite.go b/tests/e2e/testutil/suite.go index e267cd0d..4f2082c9 100644 --- a/tests/e2e/testutil/suite.go +++ b/tests/e2e/testutil/suite.go @@ -23,6 +23,20 @@ const ( IbcChannel = "channel-0" ) +// DeployedErc20 is a type that wraps the details of the pre-deployed erc20 used by the e2e test suite. +// The Address comes from SuiteConfig.KavaErc20Address +// The CosmosDenom is fetched from the EnabledConversionPairs param of x/evmutil. +// The tests expect the following: +// - the funded account has a nonzero balance of the erc20 +// - the erc20 is enabled for conversion to sdk.Coin +// - the corresponding sdk.Coin is enabled as an earn vault denom +// These requirements are checked in InitKavaEvmData(). +type DeployedErc20 struct { + Address common.Address + CosmosDenom string +} + +// E2eTestSuite is a testify test suite for running end-to-end integration tests on Kava. type E2eTestSuite struct { suite.Suite @@ -32,10 +46,12 @@ type E2eTestSuite struct { Kava *Chain Ibc *Chain - UpgradeHeight int64 - DeployedErc20Address common.Address + UpgradeHeight int64 + DeployedErc20 DeployedErc20 } +// SetupSuite is run before all tests. It initializes chain connections and sets up the +// account used for funding accounts in the tests. func (suite *E2eTestSuite) SetupSuite() { var err error fmt.Println("setting up test suite.") @@ -43,25 +59,18 @@ func (suite *E2eTestSuite) SetupSuite() { suiteConfig := ParseSuiteConfig() suite.config = suiteConfig - suite.DeployedErc20Address = common.HexToAddress(suiteConfig.KavaErc20Address) + suite.DeployedErc20 = DeployedErc20{ + Address: common.HexToAddress(suiteConfig.KavaErc20Address), + // Denom is fetched in InitKavaEvmData() + } + // setup the correct NodeRunner for the given config if suiteConfig.Kvtool != nil { - suite.UpgradeHeight = suiteConfig.Kvtool.KavaUpgradeHeight - - runnerConfig := runner.KvtoolRunnerConfig{ - KavaConfigTemplate: suiteConfig.Kvtool.KavaConfigTemplate, - - IncludeIBC: suiteConfig.IncludeIbcTests, - ImageTag: "local", - - EnableAutomatedUpgrade: suiteConfig.Kvtool.IncludeAutomatedUpgrade, - KavaUpgradeName: suiteConfig.Kvtool.KavaUpgradeName, - KavaUpgradeHeight: suiteConfig.Kvtool.KavaUpgradeHeight, - KavaUpgradeBaseImageTag: suiteConfig.Kvtool.KavaUpgradeBaseImageTag, - - SkipShutdown: suiteConfig.SkipShutdown, - } - suite.runner = runner.NewKvtoolRunner(runnerConfig) + suite.runner = suite.SetupKvtoolNodeRunner() + } else if suiteConfig.LiveNetwork != nil { + suite.runner = suite.SetupLiveNetworkNodeRunner() + } else { + panic("expected either kvtool or live network configs to be defined") } chains := suite.runner.StartChains() @@ -84,8 +93,13 @@ func (suite *E2eTestSuite) SetupSuite() { suite.InitKavaEvmData() } +// TearDownSuite is run after all tests have run. +// In the event of a panic during the tests, it is run after testify recovers. func (suite *E2eTestSuite) TearDownSuite() { fmt.Println("tearing down test suite.") + + // TODO: track asset denoms & then return all funds to initial funding account. + // close all account request channels suite.Kava.Shutdown() if suite.Ibc != nil { @@ -95,12 +109,55 @@ func (suite *E2eTestSuite) TearDownSuite() { suite.runner.Shutdown() } +// SetupKvtoolNodeRunner is a helper method for building a KvtoolRunnerConfig from the suite config. +func (suite *E2eTestSuite) SetupKvtoolNodeRunner() *runner.KvtoolRunner { + // upgrade tests are only supported on kvtool networks + suite.UpgradeHeight = suite.config.Kvtool.KavaUpgradeHeight + + runnerConfig := runner.KvtoolRunnerConfig{ + KavaConfigTemplate: suite.config.Kvtool.KavaConfigTemplate, + + IncludeIBC: suite.config.IncludeIbcTests, + ImageTag: "local", + + EnableAutomatedUpgrade: suite.config.Kvtool.IncludeAutomatedUpgrade, + KavaUpgradeName: suite.config.Kvtool.KavaUpgradeName, + KavaUpgradeHeight: suite.config.Kvtool.KavaUpgradeHeight, + KavaUpgradeBaseImageTag: suite.config.Kvtool.KavaUpgradeBaseImageTag, + + SkipShutdown: suite.config.SkipShutdown, + } + + return runner.NewKvtoolRunner(runnerConfig) +} + +// SetupLiveNetworkNodeRunner is a helper method for building a LiveNodeRunner from the suite config. +func (suite *E2eTestSuite) SetupLiveNetworkNodeRunner() *runner.LiveNodeRunner { + // live network setup doesn't presently support ibc + if suite.config.IncludeIbcTests { + panic("ibc tests not supported for live network configuration") + } + + runnerConfig := runner.LiveNodeRunnerConfig{ + KavaRpcUrl: suite.config.LiveNetwork.KavaRpcUrl, + KavaGrpcUrl: suite.config.LiveNetwork.KavaGrpcUrl, + KavaEvmRpcUrl: suite.config.LiveNetwork.KavaEvmRpcUrl, + } + + return runner.NewLiveNodeRunner(runnerConfig) +} + +// SkipIfIbcDisabled should be called at the start of tests that require IBC. +// It gracefully skips the current test if IBC tests are disabled. func (suite *E2eTestSuite) SkipIfIbcDisabled() { if !suite.config.IncludeIbcTests { suite.T().SkipNow() } } +// SkipIfUpgradeDisabled should be called at the start of tests that require automated upgrades. +// It gracefully skips the current test if upgrades are dissabled. +// Note: automated upgrade tests are currently only enabled for Kvtool suite runs. func (suite *E2eTestSuite) SkipIfUpgradeDisabled() { if suite.config.Kvtool != nil && suite.config.Kvtool.IncludeAutomatedUpgrade { suite.T().SkipNow()