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
This commit is contained in:
Robert Pirtle 2023-06-26 15:03:51 -07:00 committed by GitHub
parent 336bac7466
commit 7cff7bec23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 298 additions and 54 deletions

View File

@ -294,7 +294,6 @@ test-basic: test
# run end-to-end tests (local docker container must be built, see docker-build) # run end-to-end tests (local docker container must be built, see docker-build)
test-e2e: 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/... go test -failfast -count=1 -v ./tests/e2e/...
test: test:

View File

@ -29,5 +29,7 @@ E2E_KAVA_UPGRADE_HEIGHT=
E2E_KAVA_UPGRADE_BASE_IMAGE_TAG= E2E_KAVA_UPGRADE_BASE_IMAGE_TAG=
# E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token. # 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 E2E_KAVA_ERC20_ADDRESS=0xeA7100edA2f805356291B0E55DaD448599a72C6d

View File

@ -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

View File

@ -32,7 +32,7 @@ func setupConvertToCoinTest(
denom = tokenInfo.CosmosDenom denom = tokenInfo.CosmosDenom
initialFunds = sdk.NewCoins( initialFunds = sdk.NewCoins(
sdk.NewInt64Coin(suite.Kava.StakingDenom, 1e6), // gas money 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) user = suite.Kava.NewFundedAccount(accountName, initialFunds)
@ -40,12 +40,12 @@ func setupConvertToCoinTest(
return denom, initialFunds, user return denom, initialFunds, user
} }
// amount must be less than 1e10 // amount must be less than initial funds (1e6)
func (suite *IntegrationTestSuite) setupAccountWithCosmosCoinERC20Balance( func (suite *IntegrationTestSuite) setupAccountWithCosmosCoinERC20Balance(
accountName string, amount int64, accountName string, amount int64,
) (user *testutil.SigningAccount, contractAddress *evmutiltypes.InternalEVMAddress, denom string, sdkBalance sdk.Coins) { ) (user *testutil.SigningAccount, contractAddress *evmutiltypes.InternalEVMAddress, denom string, sdkBalance sdk.Coins) {
if amount > 1e10 { if amount > 1e6 {
panic("test erc20 amount must be less than 1e10") panic("test erc20 amount must be less than 1e6")
} }
denom, sdkBalance, user = setupConvertToCoinTest(suite, accountName) denom, sdkBalance, user = setupConvertToCoinTest(suite, accountName)
@ -88,7 +88,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() {
denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter") denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter")
fee := sdk.NewCoins(ukava(7500)) fee := sdk.NewCoins(ukava(7500))
convertAmount := int64(5e9) convertAmount := int64(5e5)
initialModuleBalance := suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) initialModuleBalance := suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom)
/////////////////////////////// ///////////////////////////////
@ -168,7 +168,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() {
func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() {
denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter-eip712") denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter-eip712")
convertAmount := int64(5e9) convertAmount := int64(5e5)
initialModuleBalance := suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) 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 // - check complex conversion flow. bob converts funds they receive on evm back to sdk.Coin
func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ERC20Magic() { func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ERC20Magic() {
fee := sdk.NewCoins(ukava(7500)) fee := sdk.NewCoins(ukava(7500))
initialAliceAmount := int64(2e6) initialAliceAmount := int64(2e5)
alice, contractAddress, denom, _ := suite.setupAccountWithCosmosCoinERC20Balance( alice, contractAddress, denom, _ := suite.setupAccountWithCosmosCoinERC20Balance(
"cosmo-coin-converter-complex-alice", initialAliceAmount, "cosmo-coin-converter-complex-alice", initialAliceAmount,
) )
gasMoney := sdk.NewCoins(ukava(1e6)) gasMoney := sdk.NewCoins(ukava(1e6))
bob := suite.Kava.NewFundedAccount("cosmo-coin-converter-complex-bob", gasMoney) 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 // bob can't move alice's funds
nonce, err := bob.NextNonce() nonce, err := bob.NextNonce()

View File

@ -22,7 +22,7 @@ func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() {
// this test manipulates state of the Greeter contract which means other tests shouldn't use it. // this test manipulates state of the Greeter contract which means other tests shouldn't use it.
// setup funded account to interact with contract // 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"] greeterAddr := suite.Kava.ContractAddrs["greeter"]
contract, err := greeter.NewGreeter(greeterAddr, suite.Kava.EvmClient) contract, err := greeter.NewGreeter(greeterAddr, suite.Kava.EvmClient)
@ -47,17 +47,17 @@ func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() {
func (suite *IntegrationTestSuite) TestEthCallToErc20() { func (suite *IntegrationTestSuite) TestEthCallToErc20() {
randoReceiver := util.SdkToEvmAddress(app.RandomAddress()) randoReceiver := util.SdkToEvmAddress(app.RandomAddress())
amount := big.NewInt(1e6) amount := big.NewInt(1e3)
// make unauthenticated eth_call query to check balance // 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 // make authenticate eth_call to transfer tokens
res := suite.FundKavaErc20Balance(randoReceiver, amount) res := suite.FundKavaErc20Balance(randoReceiver, amount)
suite.NoError(res.Err) suite.NoError(res.Err)
// make another unauthenticated eth_call query to check new balance // 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(big.NewInt(0), beforeBalance, "expected before balance to be zero")
suite.BigIntsEqual(amount, afterBalance, "unexpected post-transfer balance") suite.BigIntsEqual(amount, afterBalance, "unexpected post-transfer balance")
@ -65,12 +65,12 @@ func (suite *IntegrationTestSuite) TestEthCallToErc20() {
func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() { func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() {
// create new funded account // 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() receiver := app.RandomAddress()
// setup message for sending 1KAVA to random receiver // setup message for sending some kava to random receiver
msgs := []sdk.Msg{ msgs := []sdk.Msg{
banktypes.NewMsgSend(sender.SdkAddress, receiver, sdk.NewCoins(ukava(1e6))), banktypes.NewMsgSend(sender.SdkAddress, receiver, sdk.NewCoins(ukava(1e3))),
} }
// create tx // create tx
@ -103,16 +103,16 @@ func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() {
Denom: "ukava", Denom: "ukava",
}) })
suite.NoError(err) 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. // Note that this test works because the deployed erc20 is configured in evmutil & earn params.
func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() { func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() {
amount := sdk.NewInt(10e6) // 10 USDC amount := sdk.NewInt(1e4) // .04 USDC
sdkDenom := "erc20/multichain/usdc" sdkDenom := suite.DeployedErc20.CosmosDenom
// create new funded account // 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 // give them erc20 balance to deposit
fundRes := suite.FundKavaErc20Balance(depositor.EvmAddress, amount.BigInt()) fundRes := suite.FundKavaErc20Balance(depositor.EvmAddress, amount.BigInt())
suite.NoError(fundRes.Err) suite.NoError(fundRes.Err)
@ -121,7 +121,7 @@ func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() {
convertMsg := evmutiltypes.NewMsgConvertERC20ToCoin( convertMsg := evmutiltypes.NewMsgConvertERC20ToCoin(
evmutiltypes.NewInternalEVMAddress(depositor.EvmAddress), evmutiltypes.NewInternalEVMAddress(depositor.EvmAddress),
depositor.SdkAddress, depositor.SdkAddress,
evmutiltypes.NewInternalEVMAddress(suite.DeployedErc20Address), evmutiltypes.NewInternalEVMAddress(suite.DeployedErc20.Address),
amount, amount,
) )
depositMsg := earntypes.NewMsgDeposit( depositMsg := earntypes.NewMsgDeposit(
@ -161,7 +161,7 @@ func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() {
suite.NoError(err) suite.NoError(err)
// check that depositor no longer has erc20 balance // 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") suite.BigIntsEqual(big.NewInt(0), balance, "expected no erc20 balance")
// check that account has an earn deposit position // check that account has an earn deposit position

View File

@ -33,7 +33,7 @@ func (suite *IntegrationTestSuite) TestEthGasPriceReturnsMinFee() {
func (suite *IntegrationTestSuite) TestEvmRespectsMinFee() { func (suite *IntegrationTestSuite) TestEvmRespectsMinFee() {
// setup sender & receiver // 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()) randoReceiver := util.SdkToEvmAddress(app.RandomAddress())
// get min gas price for evm (from app.toml) // 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) // attempt tx with less than min gas price (min fee - 1)
tooLowGasPrice := minGasPrice.Sub(sdk.OneInt()).BigInt() tooLowGasPrice := minGasPrice.Sub(sdk.OneInt()).BigInt()
req := util.EvmTxRequest{ 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", Data: "this tx should fail because it's gas price is too low",
} }
res := sender.SignAndBroadcastEvmTx(req) res := sender.SignAndBroadcastEvmTx(req)

View File

@ -80,7 +80,7 @@ func (suite *IntegrationTestSuite) TestFundedAccount() {
// example test that signs & broadcasts an EVM tx // example test that signs & broadcasts an EVM tx
func (suite *IntegrationTestSuite) TestTransferOverEVM() { func (suite *IntegrationTestSuite) TestTransferOverEVM() {
// fund an account that can perform the transfer // 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)) acc := suite.Kava.NewFundedAccount("evm-test-transfer", sdk.NewCoins(initialFunds))
// get a rando account to send kava to // 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 suite.Equal(uint64(0), nonce) // sanity check. the account should have no prior txs
// transfer kava over EVM // 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{ req := util.EvmTxRequest{
Tx: ethtypes.NewTransaction(nonce, to, kavaToTransfer, 1e5, minEvmGasPrice, nil), Tx: ethtypes.NewTransaction(nonce, to, kavaToTransfer, 1e5, minEvmGasPrice, nil),
Data: "any ol' data to track this through the system", 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. // expect (9 - gas used) KAVA remaining in account.
balance := suite.Kava.QuerySdkForBalances(acc.SdkAddress) 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). // 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 // ARRANGE
// setup kava account // setup kava account
funds := ukava(1e7) // 10 KAVA funds := ukava(1e5) // .1 KAVA
kavaAcc := suite.Kava.NewFundedAccount("ibc-transfer-kava-side", sdk.NewCoins(funds)) kavaAcc := suite.Kava.NewFundedAccount("ibc-transfer-kava-side", sdk.NewCoins(funds))
// setup ibc account // setup ibc account
ibcAcc := suite.Ibc.NewFundedAccount("ibc-transfer-ibc-side", sdk.NewCoins()) ibcAcc := suite.Ibc.NewFundedAccount("ibc-transfer-ibc-side", sdk.NewCoins())
@ -127,7 +127,7 @@ func (suite *IntegrationTestSuite) TestIbcTransfer() {
gasLimit := int64(2e5) gasLimit := int64(2e5)
fee := ukava(7500) fee := ukava(7500)
fundsToSend := ukava(5e6) // 5 KAVA fundsToSend := ukava(5e4) // .005 KAVA
transferMsg := ibctypes.NewMsgTransfer( transferMsg := ibctypes.NewMsgTransfer(
testutil.IbcPort, testutil.IbcPort,
testutil.IbcChannel, testutil.IbcChannel,

View File

@ -24,22 +24,30 @@ type ChainDetails struct {
StakingDenom string StakingDenom string
} }
// EvmClient dials the underlying EVM RPC url and returns an ethclient.
func (c ChainDetails) EvmClient() (*ethclient.Client, error) { func (c ChainDetails) EvmClient() (*ethclient.Client, error) {
return ethclient.Dial(c.EvmRpcUrl) return ethclient.Dial(c.EvmRpcUrl)
} }
// GrpcConn creates a new connection to the underlying Grpc url.
func (c ChainDetails) GrpcConn() (*grpc.ClientConn, error) { func (c ChainDetails) GrpcConn() (*grpc.ClientConn, error) {
return util.NewGrpcConnection(c.GrpcUrl) 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 { type Chains struct {
byName map[string]*ChainDetails byName map[string]*ChainDetails
} }
// NewChains creates an empty Chains map.
func NewChains() Chains { func NewChains() Chains {
return Chains{byName: make(map[string]*ChainDetails, 0)} 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 { func (c Chains) MustGetChain(name string) *ChainDetails {
chain, found := c.byName[name] chain, found := c.byName[name]
if !found { if !found {
@ -48,6 +56,8 @@ func (c Chains) MustGetChain(name string) *ChainDetails {
return chain 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 { func (c *Chains) Register(name string, chain *ChainDetails) error {
if _, found := c.byName[name]; found { if _, found := c.byName[name]; found {
return ErrChainAlreadyExists return ErrChainAlreadyExists

View File

@ -32,12 +32,15 @@ type KvtoolRunner struct {
var _ NodeRunner = &KvtoolRunner{} var _ NodeRunner = &KvtoolRunner{}
// NewKvtoolRunner creates a new KvtoolRunner.
func NewKvtoolRunner(config KvtoolRunnerConfig) *KvtoolRunner { func NewKvtoolRunner(config KvtoolRunnerConfig) *KvtoolRunner {
return &KvtoolRunner{ return &KvtoolRunner{
config: config, config: config,
} }
} }
// StartChains implements NodeRunner.
// For KvtoolRunner, it sets up, runs, and connects to a local chain via kvtool.
func (k *KvtoolRunner) StartChains() Chains { func (k *KvtoolRunner) StartChains() Chains {
// install kvtool if not already installed // install kvtool if not already installed
installKvtoolCmd := exec.Command("./scripts/install-kvtool.sh") installKvtoolCmd := exec.Command("./scripts/install-kvtool.sh")
@ -89,6 +92,10 @@ func (k *KvtoolRunner) StartChains() Chains {
return 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() { func (k *KvtoolRunner) Shutdown() {
if k.config.SkipShutdown { if k.config.SkipShutdown {
log.Printf("would shut down but SkipShutdown is true") log.Printf("would shut down but SkipShutdown is true")

80
tests/e2e/runner/live.go Normal file
View File

@ -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.")
}

View File

@ -15,26 +15,28 @@ type NodeRunner interface {
Shutdown() 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 { func waitForChainStart(chainDetails ChainDetails) error {
// exponential backoff on trying to ping the node, timeout after 30 seconds // exponential backoff on trying to ping the node, timeout after 30 seconds
b := backoff.NewExponentialBackOff() b := backoff.NewExponentialBackOff()
b.MaxInterval = 5 * time.Second b.MaxInterval = 5 * time.Second
b.MaxElapsedTime = 30 * time.Second b.MaxElapsedTime = 30 * time.Second
if err := backoff.Retry(func() error { return pingKava(chainDetails.RpcUrl) }, b); err != nil { 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() b.Reset()
// the evm takes a bit longer to start up. wait for it to start as well. // 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 { 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 return nil
} }
func pingKava(rpcUrl string) error { func pingKava(rpcUrl string) error {
log.Println("pinging kava chain...")
statusUrl := fmt.Sprintf("%s/status", rpcUrl) statusUrl := fmt.Sprintf("%s/status", rpcUrl)
log.Printf("pinging kava chain: %s\n", statusUrl)
res, err := http.Get(statusUrl) res, err := http.Get(statusUrl)
if err != nil { if err != nil {
return err return err

View File

@ -27,6 +27,8 @@ import (
"github.com/kava-labs/kava/tests/util" "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 { type SigningAccount struct {
name string name string
mnemonic string mnemonic string
@ -173,6 +175,8 @@ func (a *SigningAccount) SignAndBroadcastEvmTx(req util.EvmTxRequest) EvmTxRespo
return response 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) { func (a *SigningAccount) SignRawEvmData(msg []byte) ([]byte, types.PubKey, error) {
keyringSigner := emtests.NewSigner(a.evmPrivKey) keyringSigner := emtests.NewSigner(a.evmPrivKey)
return keyringSigner.SignByAddress(a.SdkAddress, msg) return keyringSigner.SignByAddress(a.SdkAddress, msg)

View File

@ -133,6 +133,7 @@ func (chain *Chain) GetModuleBalances(moduleName string) sdk.Coins {
return chain.QuerySdkForBalances(addr) return chain.QuerySdkForBalances(addr)
} }
// GetErc20Balance fetches the ERC20 balance of `contract` for `address`.
func (chain *Chain) GetErc20Balance(contract, address common.Address) *big.Int { func (chain *Chain) GetErc20Balance(contract, address common.Address) *big.Int {
resData, err := chain.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ resData, err := chain.EvmClient.CallContract(context.Background(), ethereum.CallMsg{
To: &contract, To: &contract,

View File

@ -13,12 +13,15 @@ func init() {
gotenv.Load() gotenv.Load()
} }
// SuiteConfig wraps configuration details for running the end-to-end test suite.
type SuiteConfig struct { type SuiteConfig struct {
// A funded account used to fnd all other accounts. // A funded account used to fnd all other accounts.
FundedAccountMnemonic string FundedAccountMnemonic string
// A config for using kvtool local networks for the test run // A config for using kvtool local networks for the test run
Kvtool *KvtoolConfig 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. // Whether or not to start an IBC chain. Use `suite.SkipIfIbcDisabled()` in IBC tests in IBC tests.
IncludeIbcTests bool IncludeIbcTests bool
@ -30,6 +33,8 @@ type SuiteConfig struct {
SkipShutdown bool 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 { type KvtoolConfig struct {
// The kava.configTemplate flag to be passed to kvtool, usually "master". // The kava.configTemplate flag to be passed to kvtool, usually "master".
// This allows one to change the base genesis used to start the chain. // This allows one to change the base genesis used to start the chain.
@ -45,6 +50,15 @@ type KvtoolConfig struct {
KavaUpgradeBaseImageTag string 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 { func ParseSuiteConfig() SuiteConfig {
config := SuiteConfig{ config := SuiteConfig{
// this mnemonic is expected to be a funded account that can seed the funds for all // 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 { if useKvtoolNetworks {
kvtoolConfig := ParseKvtoolConfig() kvtoolConfig := ParseKvtoolConfig()
config.Kvtool = &kvtoolConfig config.Kvtool = &kvtoolConfig
} else {
liveNetworkConfig := ParseLiveNetworkConfig()
config.LiveNetwork = &liveNetworkConfig
} }
return config return config
} }
// ParseKvtoolConfig builds a KvtoolConfig from environment variables.
func ParseKvtoolConfig() KvtoolConfig { func ParseKvtoolConfig() KvtoolConfig {
config := KvtoolConfig{ config := KvtoolConfig{
KavaConfigTemplate: nonemptyStringEnv("E2E_KVTOOL_KAVA_CONFIG_TEMPLATE"), KavaConfigTemplate: nonemptyStringEnv("E2E_KVTOOL_KAVA_CONFIG_TEMPLATE"),
@ -87,6 +105,17 @@ func ParseKvtoolConfig() KvtoolConfig {
return config 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 { func mustParseBool(name string) bool {
envValue := os.Getenv(name) envValue := os.Getenv(name)
if envValue == "" { if envValue == "" {
@ -99,6 +128,8 @@ func mustParseBool(name string) bool {
return value return value
} }
// nonemptyStringEnv is a helper method that panics if the env variable `name`
// is empty or undefined.
func nonemptyStringEnv(name string) string { func nonemptyStringEnv(name string) string {
value := os.Getenv(name) value := os.Getenv(name)
if value == "" { if value == "" {

View File

@ -16,6 +16,8 @@ import (
evmtypes "github.com/evmos/ethermint/x/evm/types" 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( func (suite *E2eTestSuite) NewEip712TxBuilder(
acc *SigningAccount, chain *Chain, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg, memo string, acc *SigningAccount, chain *Chain, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg, memo string,
) client.TxBuilder { ) client.TxBuilder {

View File

@ -10,6 +10,8 @@ import (
"github.com/kava-labs/kava/tests/e2e/contracts/greeter" "github.com/kava-labs/kava/tests/e2e/contracts/greeter"
"github.com/kava-labs/kava/tests/util" "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. // 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) whale := suite.Kava.GetAccount(FundedAccountName)
// ensure funded account has nonzero erc20 balance // 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 { if balance.Cmp(big.NewInt(0)) != 1 {
panic(fmt.Sprintf("expected funded account (%s) to have erc20 balance", whale.EvmAddress.Hex())) 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 // deploy an example contract
greeterAddr, _, _, err := greeter.DeployGreeter( greeterAddr, _, _, err := greeter.DeployGreeter(
whale.evmSigner.Auth, whale.evmSigner.Auth,
@ -33,6 +61,7 @@ func (suite *E2eTestSuite) InitKavaEvmData() {
suite.Kava.ContractAddrs["greeter"] = greeterAddr 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 { func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount *big.Int) EvmTxResponse {
// funded account should have erc20 balance // funded account should have erc20 balance
whale := suite.Kava.GetAccount(FundedAccountName) whale := suite.Kava.GetAccount(FundedAccountName)
@ -42,7 +71,7 @@ func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount
suite.NoError(err) suite.NoError(err)
req := util.EvmTxRequest{ 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()), Data: fmt.Sprintf("fund %s with ERC20 balance (%s)", toAddress.Hex(), amount.String()),
} }

View File

@ -23,6 +23,20 @@ const (
IbcChannel = "channel-0" 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 { type E2eTestSuite struct {
suite.Suite suite.Suite
@ -33,9 +47,11 @@ type E2eTestSuite struct {
Ibc *Chain Ibc *Chain
UpgradeHeight int64 UpgradeHeight int64
DeployedErc20Address common.Address 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() { func (suite *E2eTestSuite) SetupSuite() {
var err error var err error
fmt.Println("setting up test suite.") fmt.Println("setting up test suite.")
@ -43,25 +59,18 @@ func (suite *E2eTestSuite) SetupSuite() {
suiteConfig := ParseSuiteConfig() suiteConfig := ParseSuiteConfig()
suite.config = suiteConfig suite.config = suiteConfig
suite.DeployedErc20Address = common.HexToAddress(suiteConfig.KavaErc20Address) suite.DeployedErc20 = DeployedErc20{
Address: common.HexToAddress(suiteConfig.KavaErc20Address),
if suiteConfig.Kvtool != nil { // Denom is fetched in InitKavaEvmData()
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)
// setup the correct NodeRunner for the given config
if suiteConfig.Kvtool != nil {
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() chains := suite.runner.StartChains()
@ -84,8 +93,13 @@ func (suite *E2eTestSuite) SetupSuite() {
suite.InitKavaEvmData() 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() { func (suite *E2eTestSuite) TearDownSuite() {
fmt.Println("tearing down test suite.") fmt.Println("tearing down test suite.")
// TODO: track asset denoms & then return all funds to initial funding account.
// close all account request channels // close all account request channels
suite.Kava.Shutdown() suite.Kava.Shutdown()
if suite.Ibc != nil { if suite.Ibc != nil {
@ -95,12 +109,55 @@ func (suite *E2eTestSuite) TearDownSuite() {
suite.runner.Shutdown() 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() { func (suite *E2eTestSuite) SkipIfIbcDisabled() {
if !suite.config.IncludeIbcTests { if !suite.config.IncludeIbcTests {
suite.T().SkipNow() 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() { func (suite *E2eTestSuite) SkipIfUpgradeDisabled() {
if suite.config.Kvtool != nil && suite.config.Kvtool.IncludeAutomatedUpgrade { if suite.config.Kvtool != nil && suite.config.Kvtool.IncludeAutomatedUpgrade {
suite.T().SkipNow() suite.T().SkipNow()