From 9430be71d2e7d1720473462558548e87e5d62c62 Mon Sep 17 00:00:00 2001 From: kosegor <30661385+kosegor@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:22:29 -0300 Subject: [PATCH] test: metoken e2e (#2232) * added util func for meToken rest * metoken and leverage setup with initial tests * swap e2e test * fix txs * finished e2e tests * PR refactor * Revert "PR refactor" This reverts commit 142d0aa981d1082a1eb6009510953e5ea077315c. * PR changes * pr comments --------- Co-authored-by: Adam Moser <63419657+toteki@users.noreply.github.com> --- tests/e2e/e2e_metoken_test.go | 223 ++++++++++++++++++++++++++++++++++ tests/e2e/setup/keys.go | 3 +- tests/e2e/setup/metoken.go | 43 +++++++ tests/e2e/setup/utils.go | 52 +++++--- tests/grpc/gov.go | 45 ++++++- 5 files changed, 349 insertions(+), 17 deletions(-) create mode 100644 tests/e2e/e2e_metoken_test.go create mode 100644 tests/e2e/setup/metoken.go diff --git a/tests/e2e/e2e_metoken_test.go b/tests/e2e/e2e_metoken_test.go new file mode 100644 index 0000000000..e215ab40e0 --- /dev/null +++ b/tests/e2e/e2e_metoken_test.go @@ -0,0 +1,223 @@ +package e2e + +import ( + "strings" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v6/app" + "github.com/umee-network/umee/v6/tests/grpc" + ltypes "github.com/umee-network/umee/v6/x/leverage/types" + "github.com/umee-network/umee/v6/x/metoken" + "github.com/umee-network/umee/v6/x/metoken/mocks" +) + +func (s *E2ETest) TestMetokenSwapAndRedeem() { + var prices []metoken.IndexPrices + var index metoken.Index + valAddr, err := s.Chain.Validators[0].KeyInfo.GetAddress() + s.Require().NoError(err) + expectedBalance := mocks.EmptyUSDIndexBalances(mocks.MeUSDDenom) + + if app.Experimental { + s.T().Skip("Skipping tests for experimental module x/metoken") + } + + s.Run( + "create_stable_index", func() { + tokens := []ltypes.Token{ + mocks.ValidToken(mocks.USDTBaseDenom, mocks.USDTSymbolDenom, 6), + mocks.ValidToken(mocks.USDCBaseDenom, mocks.USDCSymbolDenom, 6), + mocks.ValidToken(mocks.ISTBaseDenom, mocks.ISTSymbolDenom, 6), + } + + err = grpc.LeverageRegistryUpdate(s.Umee, tokens, nil) + s.Require().NoError(err) + + meUSD := mocks.StableIndex(mocks.MeUSDDenom) + err = grpc.MetokenRegistryUpdate(s.Umee, []metoken.Index{meUSD}, nil) + s.Require().NoError(err) + + prices = s.checkMetokenBalance(meUSD.Denom, expectedBalance) + }, + ) + + s.Run( + "swap_100USDT_success", func() { + index = s.getMetokenIndex(mocks.MeUSDDenom) + + hundredUSDT := sdk.NewCoin(mocks.USDTBaseDenom, sdkmath.NewInt(100_000000)) + fee := index.Fee.MinFee.MulInt(hundredUSDT.Amount).TruncateInt() + + assetSettings, i := index.AcceptedAsset(mocks.USDTBaseDenom) + s.Require().True(i >= 0) + + amountToSwap := hundredUSDT.Amount.Sub(fee) + amountToReserves := assetSettings.ReservePortion.MulInt(amountToSwap).TruncateInt() + amountToLeverage := amountToSwap.Sub(amountToReserves) + + usdtPrice, err := prices[0].PriceByBaseDenom(mocks.USDTBaseDenom) + s.Require().NoError(err) + returned := usdtPrice.SwapRate.MulInt(amountToSwap).TruncateInt() + + s.executeSwap(valAddr.String(), hundredUSDT, mocks.MeUSDDenom) + + expectedBalance.MetokenSupply.Amount = expectedBalance.MetokenSupply.Amount.Add(returned) + usdtBalance, i := expectedBalance.AssetBalance(mocks.USDTBaseDenom) + s.Require().True(i >= 0) + usdtBalance.Fees = usdtBalance.Fees.Add(fee) + usdtBalance.Reserved = usdtBalance.Reserved.Add(amountToReserves) + usdtBalance.Leveraged = usdtBalance.Leveraged.Add(amountToLeverage) + expectedBalance.SetAssetBalance(usdtBalance) + + prices = s.checkMetokenBalance(mocks.MeUSDDenom, expectedBalance) + }, + ) + + s.Run( + "redeem_200meUSD_failure", func() { + twoHundredsMeUSD := sdk.NewCoin(mocks.MeUSDDenom, sdkmath.NewInt(200_000000)) + + s.executeRedeemWithFailure( + valAddr.String(), + twoHundredsMeUSD, + mocks.USDTBaseDenom, + "not enough", + ) + + prices = s.checkMetokenBalance(mocks.MeUSDDenom, expectedBalance) + }, + ) + + s.Run( + "redeem_50meUSD_success", func() { + fiftyMeUSD := sdk.NewCoin(mocks.MeUSDDenom, sdkmath.NewInt(50_000000)) + + s.executeRedeemSuccess(valAddr.String(), fiftyMeUSD, mocks.USDTBaseDenom) + + usdtPrice, err := prices[0].PriceByBaseDenom(mocks.USDTBaseDenom) + s.Require().NoError(err) + usdtToRedeem := usdtPrice.RedeemRate.MulInt(fiftyMeUSD.Amount).TruncateInt() + fee := index.Fee.MinFee.MulInt(usdtToRedeem).TruncateInt() + + assetSettings, i := index.AcceptedAsset(mocks.USDTBaseDenom) + s.Require().True(i >= 0) + amountFromReserves := assetSettings.ReservePortion.MulInt(usdtToRedeem).TruncateInt() + amountFromLeverage := usdtToRedeem.Sub(amountFromReserves) + + expectedBalance.MetokenSupply.Amount = expectedBalance.MetokenSupply.Amount.Sub(fiftyMeUSD.Amount) + usdtBalance, i := expectedBalance.AssetBalance(mocks.USDTBaseDenom) + s.Require().True(i >= 0) + usdtBalance.Fees = usdtBalance.Fees.Add(fee) + usdtBalance.Reserved = usdtBalance.Reserved.Sub(amountFromReserves) + usdtBalance.Leveraged = usdtBalance.Leveraged.Sub(amountFromLeverage) + expectedBalance.SetAssetBalance(usdtBalance) + + _ = s.checkMetokenBalance(mocks.MeUSDDenom, expectedBalance) + }, + ) +} + +func (s *E2ETest) checkMetokenBalance(denom string, expectedBalance metoken.IndexBalances) []metoken.IndexPrices { + var prices []metoken.IndexPrices + s.Require().Eventually( + func() bool { + resp, err := s.QueryMetokenBalances(denom) + if err != nil { + return false + } + + var exist bool + for _, balance := range resp.IndexBalances { + if balance.MetokenSupply.Denom == expectedBalance.MetokenSupply.Denom { + exist = true + s.Require().Equal(expectedBalance, balance) + break + } + } + + s.Require().True(exist) + prices = resp.Prices + return true + }, + 30*time.Second, + 500*time.Millisecond, + ) + + return prices +} + +func (s *E2ETest) getMetokenIndex(denom string) metoken.Index { + index := metoken.Index{} + s.Require().Eventually( + func() bool { + resp, err := s.QueryMetokenIndexes(denom) + if err != nil { + return false + } + + var exist bool + for _, indx := range resp.Registry { + if indx.Denom == denom { + exist = true + index = indx + break + } + } + + s.Require().True(exist) + return true + }, + 30*time.Second, + 500*time.Millisecond, + ) + + return index +} + +func (s *E2ETest) executeSwap(umeeAddr string, asset sdk.Coin, meTokenDenom string) { + s.Require().Eventually( + func() bool { + err := s.TxMetokenSwap(umeeAddr, asset, meTokenDenom) + if err != nil { + return false + } + + return true + }, + 30*time.Second, + 500*time.Millisecond, + ) +} + +func (s *E2ETest) executeRedeemSuccess(umeeAddr string, meToken sdk.Coin, assetDenom string) { + s.Require().Eventually( + func() bool { + err := s.TxMetokenRedeem(umeeAddr, meToken, assetDenom) + if err != nil { + return false + } + + return true + }, + 30*time.Second, + 500*time.Millisecond, + ) +} + +func (s *E2ETest) executeRedeemWithFailure(umeeAddr string, meToken sdk.Coin, assetDenom, errMsg string) { + s.Require().Eventually( + func() bool { + err := s.TxMetokenRedeem(umeeAddr, meToken, assetDenom) + if err != nil && strings.Contains(err.Error(), errMsg) { + return true + } + + return false + }, + 30*time.Second, + 500*time.Millisecond, + ) +} diff --git a/tests/e2e/setup/keys.go b/tests/e2e/setup/keys.go index 17168ce70a..5dfcd5b4c0 100644 --- a/tests/e2e/setup/keys.go +++ b/tests/e2e/setup/keys.go @@ -7,11 +7,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/go-bip39" appparams "github.com/umee-network/umee/v6/app/params" + "github.com/umee-network/umee/v6/x/metoken/mocks" ) const ( PhotonDenom = "photon" - InitBalanceStr = "510000000000" + appparams.BondDenom + ",100000000000" + PhotonDenom + InitBalanceStr = "510000000000" + appparams.BondDenom + ",100000000000" + PhotonDenom + ",100000000000" + mocks.USDTBaseDenom GaiaChainID = "test-gaia-chain" EthChainID uint = 15 diff --git a/tests/e2e/setup/metoken.go b/tests/e2e/setup/metoken.go new file mode 100644 index 0000000000..4f5baed802 --- /dev/null +++ b/tests/e2e/setup/metoken.go @@ -0,0 +1,43 @@ +package setup + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v6/x/metoken" +) + +func (s *E2ETestSuite) QueryMetokenBalances(denom string) (metoken.QueryIndexBalancesResponse, error) { + endpoint := fmt.Sprintf("%s/umee/metoken/v1/index_balances?metoken_denom=%s", s.UmeeREST(), denom) + var resp metoken.QueryIndexBalancesResponse + + return resp, s.QueryREST(endpoint, &resp) +} + +func (s *E2ETestSuite) QueryMetokenIndexes(denom string) (metoken.QueryIndexesResponse, error) { + endpoint := fmt.Sprintf("%s/umee/metoken/v1/indexes?metoken_denom=%s", s.UmeeREST(), denom) + var resp metoken.QueryIndexesResponse + + return resp, s.QueryREST(endpoint, &resp) +} + +func (s *E2ETestSuite) TxMetokenSwap(umeeAddr string, asset sdk.Coin, meTokenDenom string) error { + req := &metoken.MsgSwap{ + User: umeeAddr, + Asset: asset, + MetokenDenom: meTokenDenom, + } + + return s.broadcastTxWithRetry(req) +} + +func (s *E2ETestSuite) TxMetokenRedeem(umeeAddr string, meToken sdk.Coin, assetDenom string) error { + req := &metoken.MsgRedeem{ + User: umeeAddr, + Metoken: meToken, + AssetDenom: assetDenom, + } + + return s.broadcastTxWithRetry(req) +} diff --git a/tests/e2e/setup/utils.go b/tests/e2e/setup/utils.go index c421c88e07..b7a31568c7 100644 --- a/tests/e2e/setup/utils.go +++ b/tests/e2e/setup/utils.go @@ -135,21 +135,7 @@ func (s *E2ETestSuite) QueryREST(endpoint string, valPtr interface{}) error { return fmt.Errorf("tx query returned non-200 status: %d (%s)", resp.StatusCode, endpoint) } - if valProto, ok := valPtr.(proto.Message); ok { - bz, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w, endpoint: %s", err, endpoint) - } - if err = s.cdc.UnmarshalJSON(bz, valProto); err != nil { - return fmt.Errorf("failed to protoJSON.decode response body: %w, endpoint: %s", err, endpoint) - } - } else { - if err := json.NewDecoder(resp.Body).Decode(valPtr); err != nil { - return fmt.Errorf("failed to json.decode response body: %w, endpoint: %s", err, endpoint) - } - } - - return nil + return decodeRespBody(s.cdc, endpoint, resp.Body, valPtr) } func (s *E2ETestSuite) QueryUmeeTx(endpoint, txHash string) error { @@ -245,6 +231,24 @@ func (s *E2ETestSuite) QueryUmeeBalance( return umeeBalance, umeeAddr } +func (s *E2ETestSuite) broadcastTxWithRetry(msg sdk.Msg) error { + var err error + for retry := 0; retry < 3; retry++ { + // retry if txs fails, because sometimes account sequence mismatch occurs due to txs pending + _, err = s.Umee.Client.Tx.BroadcastTx(msg) + if err == nil { + return nil + } + + if err != nil && !strings.Contains(err.Error(), "incorrect account sequence") { + return err + } + time.Sleep(time.Millisecond * 300) + } + + return err +} + func decodeTx(cdc codec.Codec, txBytes []byte) (*sdktx.Tx, error) { var raw sdktx.TxRaw @@ -281,3 +285,21 @@ func decodeTx(cdc codec.Codec, txBytes []byte) (*sdktx.Tx, error) { Signatures: raw.Signatures, }, nil } + +func decodeRespBody(cdc codec.Codec, endpoint string, body io.ReadCloser, valPtr interface{}) error { + if valProto, ok := valPtr.(proto.Message); ok { + bz, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("failed to read response body: %w, endpoint: %s", err, endpoint) + } + if err = cdc.UnmarshalJSON(bz, valProto); err != nil { + return fmt.Errorf("failed to protoJSON.decode response body: %w, endpoint: %s", err, endpoint) + } + } else { + if err := json.NewDecoder(body).Decode(valPtr); err != nil { + return fmt.Errorf("failed to json.decode response body: %w, endpoint: %s", err, endpoint) + } + } + + return nil +} diff --git a/tests/grpc/gov.go b/tests/grpc/gov.go index 3906e70652..6e3f0fe702 100644 --- a/tests/grpc/gov.go +++ b/tests/grpc/gov.go @@ -6,10 +6,12 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - proposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + "github.com/cosmos/cosmos-sdk/x/params/types/proposal" "github.com/umee-network/umee/v6/client" "github.com/umee-network/umee/v6/util/checkers" + ltypes "github.com/umee-network/umee/v6/x/leverage/types" + "github.com/umee-network/umee/v6/x/metoken" "github.com/umee-network/umee/v6/x/uibc" ) @@ -75,6 +77,47 @@ func UIBCIBCTransferSatusUpdate(umeeClient client.Client, status uibc.IBCTransfe return MakeVoteAndCheckProposal(umeeClient, *resp) } +// LeverageRegistryUpdate submits a gov transaction to update leverage registry, votes, and waits for proposal to pass. +func LeverageRegistryUpdate(umeeClient client.Client, addTokens, updateTokens []ltypes.Token) error { + msg := ltypes.MsgGovUpdateRegistry{ + Authority: checkers.GovModuleAddr, + Description: "", + AddTokens: addTokens, + UpdateTokens: updateTokens, + } + + resp, err := umeeClient.Tx.TxSubmitProposalWithMsg([]sdk.Msg{&msg}) + if err != nil { + return err + } + + if len(resp.Logs) == 0 { + return fmt.Errorf("no logs in response") + } + + return MakeVoteAndCheckProposal(umeeClient, *resp) +} + +// MetokenRegistryUpdate submits a gov transaction to update metoken registry, votes, and waits for proposal to pass. +func MetokenRegistryUpdate(umeeClient client.Client, addIndexes, updateIndexes []metoken.Index) error { + msg := metoken.MsgGovUpdateRegistry{ + Authority: checkers.GovModuleAddr, + AddIndex: addIndexes, + UpdateIndex: updateIndexes, + } + + resp, err := umeeClient.Tx.TxSubmitProposalWithMsg([]sdk.Msg{&msg}) + if err != nil { + return err + } + + if len(resp.Logs) == 0 { + return fmt.Errorf("no logs in response") + } + + return MakeVoteAndCheckProposal(umeeClient, *resp) +} + func MakeVoteAndCheckProposal(umeeClient client.Client, resp sdk.TxResponse) error { var proposalID string for _, event := range resp.Logs[0].Events {