diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f8fafde..3b27d0738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#858](https://github.com/NibiruChain/nibiru/pull/858) - fix trading limit ratio check; checks in both directions on both quote and base assets * [#865](https://github.com/NibiruChain/nibiru/pull/865) - refactor(vpool): clean up interface for CmdGetBaseAssetPrice to use add and remove as directions +* [#868](https://github.com/NibiruChain/nibiru/pull/868) - refactor dex integration tests to be independent between them ### Features diff --git a/Makefile b/Makefile index 8eb773cad..78cafd565 100644 --- a/Makefile +++ b/Makefile @@ -167,7 +167,7 @@ test-sim-benchmark-invariants: ############################################################################### lint: - docker run -v $(CURDIR):/code -w /code golangci/golangci-lint:v1.47.3-alpine golangci-lint run + docker run -v $(CURDIR):/code --rm -w /code golangci/golangci-lint:v1.47.3-alpine golangci-lint run .PHONY: \ test-sim-nondeterminism \ diff --git a/x/dex/client/cli/flags.go b/x/dex/client/cli/flags.go index 32f1e6aaf..ce1cf18ab 100644 --- a/x/dex/client/cli/flags.go +++ b/x/dex/client/cli/flags.go @@ -5,22 +5,22 @@ import ( ) const ( - // Will be parsed to string. + // FlagPoolFile Will be parsed to string. FlagPoolFile = "pool-file" - // Will be parsed to uint64. + // FlagPoolId Will be parsed to uint64. FlagPoolId = "pool-id" - // Will be parsed to []sdk.Coin. + // FlagTokensIn Will be parsed to []sdk.Coin. FlagTokensIn = "tokens-in" - // Will be parsed to sdk.Coin. + // FlagPoolSharesOut Will be parsed to sdk.Coin. FlagPoolSharesOut = "pool-shares-out" - // Will be parsed to sdk.Coin. + // FlagTokenIn Will be parsed to sdk.Coin. FlagTokenIn = "token-in" - // Will be parsed to string. + // FlagTokenOutDenom Will be parsed to string. FlagTokenOutDenom = "token-out-denom" ) diff --git a/x/dex/client/testutil/cli_test.go b/x/dex/client/testutil/cli_test.go index d911726b5..5dd57931c 100644 --- a/x/dex/client/testutil/cli_test.go +++ b/x/dex/client/testutil/cli_test.go @@ -1,520 +1,45 @@ -package cli_test +package testutil import ( - "fmt" "testing" - "github.com/NibiruChain/nibiru/simapp" - - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/crypto/hd" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" sdk "github.com/cosmos/cosmos-sdk/types" - banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/suite" - tmcli "github.com/tendermint/tendermint/libs/cli" "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/simapp" "github.com/NibiruChain/nibiru/x/common" - "github.com/NibiruChain/nibiru/x/dex/client/cli" - "github.com/NibiruChain/nibiru/x/dex/types" testutilcli "github.com/NibiruChain/nibiru/x/testutil/cli" ) -type IntegrationTestSuite struct { - suite.Suite - - cfg testutilcli.Config - network *testutilcli.Network - - testAccount sdk.AccAddress -} - -func (s *IntegrationTestSuite) SetupSuite() { - /* Make test skip if -short is not used: - All tests: `go test ./...` - Unit tests only: `go test ./... -short` - Integration tests only: `go test ./... -run Integration` - https://stackoverflow.com/a/41407042/13305627 */ - if testing.Short() { - s.T().Skip("skipping integration test suite") +func TestIntegrationTestSuite(t *testing.T) { + coinsFromGenesis := []string{ + common.DenomGov, + common.DenomStable, + common.DenomColl, + "coin-1", + "coin-2", + "coin-3", + "coin-4", + "coin-5", } - s.T().Log("setting up integration test suite") - app.SetPrefixes(app.AccountAddressPrefix) genesisState := simapp.NewTestGenesisStateFromDefault() - s.cfg = testutilcli.BuildNetworkConfig(genesisState) - s.cfg.StartingTokens = sdk.NewCoins( - sdk.NewInt64Coin(common.DenomStable, 20000), - sdk.NewInt64Coin(common.DenomColl, 20000), - sdk.NewInt64Coin(common.DenomGov, 2e12), // for pool creation fee and more for tx fees - ) - - s.network = testutilcli.NewNetwork(s.T(), s.cfg) - _, err := s.network.WaitForHeight(1) - s.NoError(err) - - val := s.network.Validators[0] - info, _, err := val.ClientCtx.Keyring.NewMnemonic("user1", keyring.English, sdk.FullFundraiserPath, "", hd.Secp256k1) - s.NoError(err) - user1 := sdk.AccAddress(info.GetPubKey().Address()) - - // create a new user address - s.testAccount = user1 - _, err = testutilcli.FillWalletFromValidator(user1, - sdk.NewCoins( - sdk.NewInt64Coin(common.DenomStable, 20000), - sdk.NewInt64Coin(common.DenomColl, 20000), - sdk.NewInt64Coin(common.DenomGov, 2e9), // for pool creation fee and more for tx fees - ), - val, - common.DenomGov, + genesisState = WhitelistGenesisAssets( + genesisState, + coinsFromGenesis, ) - s.Require().NoError(err) -} - -func (s *IntegrationTestSuite) TearDownSuite() { - s.T().Log("tearing down integration test suite") - s.network.Cleanup() -} - -func (s IntegrationTestSuite) TestACreatePoolCmd() { - val := s.network.Validators[0] - testCases := []struct { - name string - tokenWeights string - initialDeposit string - swapFee string - exitFee string - extraArgs []string - expectedErr error - respType proto.Message - expectedCode uint32 - queryexpectedPass bool - queryexpectedErr string - queryArgs []string - }{ - { - name: "create pool with insufficient funds", - tokenWeights: fmt.Sprintf("1%s, 1%s", common.DenomGov, common.DenomStable), - initialDeposit: fmt.Sprintf("1000000000%s,10000000000%s", common.DenomGov, common.DenomStable), - swapFee: "0.003", - exitFee: "0.003", - extraArgs: []string{}, - respType: &sdk.TxResponse{}, - expectedCode: 5, // bankKeeper code for insufficient funds - queryexpectedPass: false, - queryexpectedErr: "pool not found", - queryArgs: []string{"1"}, - }, - { - name: "create pool with invalid weights", - tokenWeights: fmt.Sprintf("0%s, 1%s", common.DenomGov, common.DenomStable), - initialDeposit: fmt.Sprintf("10000%s,10000%s", common.DenomGov, common.DenomStable), - swapFee: "0.003", - exitFee: "0.003", - extraArgs: []string{}, - expectedErr: types.ErrInvalidCreatePoolArgs, - queryexpectedPass: false, - queryexpectedErr: "pool not found", - queryArgs: []string{"1"}, - }, - { - name: "create pool with deposit not matching weights", - tokenWeights: "1unibi, 1uusdc", - initialDeposit: "1000foo,10000uusdc", - swapFee: "0.003", - exitFee: "0.003", - extraArgs: []string{}, - expectedErr: types.ErrInvalidCreatePoolArgs, - queryexpectedPass: false, - queryexpectedErr: "pool not found", - queryArgs: []string{"1"}, - }, - { - name: "create pool with sufficient funds", - tokenWeights: "1unibi,1uusdc", - initialDeposit: "100unibi,100uusdc", - swapFee: "0.01", - exitFee: "0.01", - extraArgs: []string{}, - respType: &sdk.TxResponse{}, - expectedCode: 0, - queryexpectedPass: true, - queryArgs: []string{"1"}, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - out, err := ExecMsgCreatePool(s.T(), val.ClientCtx, s.testAccount, tc.tokenWeights, tc.initialDeposit, tc.swapFee, tc.exitFee, tc.extraArgs...) - if tc.expectedErr != nil { - s.Require().ErrorIs(err, tc.expectedErr) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) - - // Query balance - cmd := cli.CmdTotalPoolLiquidity() - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, tc.queryArgs) - - if !tc.queryexpectedPass { - s.Require().Contains(out.String(), tc.queryexpectedErr) - } else { - resp := types.QueryTotalPoolLiquidityResponse{} - s.Require().NoError(err, out.String()) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String()) - } - } - }) - } -} - -func (s IntegrationTestSuite) TestBNewJoinPoolCmd() { - val := s.network.Validators[0] - - // create a new pool - _, err := ExecMsgCreatePool( - s.T(), - val.ClientCtx, - /*owner-*/ val.Address, - /*tokenWeights=*/ "5unibi,5uusdc", - /*initialDeposit=*/ "100unibi,100uusdc", - /*swapFee=*/ "0.01", - /*exitFee=*/ "0.01", + cfg := testutilcli.BuildNetworkConfig(genesisState) + cfg.StartingTokens = sdk.NewCoins( + sdk.NewInt64Coin(common.DenomGov, 2e12), // for pool creation fee and more for tx fees ) - s.Require().NoError(err) - - testCases := []struct { - name string - poolId uint64 - tokensIn string - expectErr bool - respType proto.Message - expectedCode uint32 - }{ - { - name: "join pool with insufficient balance", - poolId: 1, - tokensIn: "1000000000unibi,10000000000uusdc", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 5, // bankKeeper code for insufficient funds - }, - { - name: "join pool with sufficient balance", - poolId: 1, - tokensIn: "100unibi,100uusdc", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - ctx := val.ClientCtx - - out, err := ExecMsgJoinPool(s.T(), ctx, tc.poolId, s.testAccount, tc.tokensIn) - if tc.expectErr { - s.Require().Error(err) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) - } - }) - } -} - -func (s IntegrationTestSuite) TestCNewExitPoolCmd() { - val := s.network.Validators[0] - testCases := []struct { - name string - poolId uint64 - poolSharesOut string - expectErr bool - respType proto.Message - expectedCode uint32 - expectedunibi sdk.Int - expectedOtherToken sdk.Int - }{ - { - name: "exit pool from invalid pool", - poolId: 2, - poolSharesOut: "100nibiru/pool/1", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 1, // dex.types.ErrNonExistingPool - expectedunibi: sdk.NewInt(-10), - expectedOtherToken: sdk.NewInt(0), - }, - { - name: "exit pool for too many shares", - poolId: 1, - poolSharesOut: "1001000000000000000000nibiru/pool/1", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 1, - expectedunibi: sdk.NewInt(-10), - expectedOtherToken: sdk.NewInt(0), - }, - { - name: "exit pool for zero shares", - poolId: 1, - poolSharesOut: "0nibiru/pool/1", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 1, - expectedunibi: sdk.NewInt(-10), - expectedOtherToken: sdk.NewInt(0), - }, - { - name: "exit pool with sufficient balance", - poolId: 1, - poolSharesOut: "101000000000000000000nibiru/pool/1", - expectErr: false, - respType: &sdk.TxResponse{}, - expectedCode: 0, - expectedunibi: sdk.NewInt(100 - 10 - 1), // Received unibi minus 10unibi tx fee minus 1 exit pool fee - expectedOtherToken: sdk.NewInt(100 - 1), // Received uusdc minus 1 exit pool fee - }, + for _, coin := range coinsFromGenesis { + cfg.StartingTokens = cfg.StartingTokens.Add(sdk.NewInt64Coin(coin, 40000)) } - for _, tc := range testCases { - tc := tc - ctx := val.ClientCtx - - s.Run(tc.name, func() { - // Get original balance - resp, err := banktestutil.QueryBalancesExec(ctx, s.testAccount) - s.Require().NoError(err) - var originalBalance banktypes.QueryAllBalancesResponse - s.Require().NoError(ctx.Codec.UnmarshalJSON(resp.Bytes(), &originalBalance)) - - out, err := ExecMsgExitPool(s.T(), ctx, tc.poolId, s.testAccount, tc.poolSharesOut) - - if tc.expectErr { - s.Require().Error(err) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) - - // Ensure balance is ok - resp, err := banktestutil.QueryBalancesExec(ctx, s.testAccount) - s.Require().NoError(err) - var finalBalance banktypes.QueryAllBalancesResponse - s.Require().NoError(ctx.Codec.UnmarshalJSON(resp.Bytes(), &finalBalance)) - - s.Require().Equal( - originalBalance.Balances.AmountOf("uusdc").Add(tc.expectedOtherToken), - finalBalance.Balances.AmountOf("uusdc"), - ) - s.Require().Equal( - originalBalance.Balances.AmountOf("unibi").Add(tc.expectedunibi), - finalBalance.Balances.AmountOf("unibi"), - ) - } - }) - } -} - -func (s *IntegrationTestSuite) TestDGetCmdTotalLiquidity() { - val := s.network.Validators[0] - - testCases := []struct { - name string - args []string - expectErr bool - }{ - { - "query total liquidity", // nibid query dex total-liquidity - []string{ - fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"), - }, - false, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := cli.CmdTotalLiquidity() - clientCtx := val.ClientCtx - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Error(err) - } else { - resp := types.QueryTotalLiquidityResponse{} - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String()) - } - }) - } -} - -func (s *IntegrationTestSuite) TestESwapAssets() { - val := s.network.Validators[0] - - testCases := []struct { - name string - poolId uint64 - tokenIn string - tokenOutDenom string - respType proto.Message - expectedCode uint32 - expectErr bool - }{ - { - name: "zero pool id", - poolId: 0, - tokenIn: "50unibi", - tokenOutDenom: "uusdc", - expectErr: true, - }, - { - name: "invalid token in", - poolId: 1, - tokenIn: "0unibi", - tokenOutDenom: "uusdc", - expectErr: true, - }, - { - name: "invalid token out denom", - poolId: 1, - tokenIn: "50unibi", - tokenOutDenom: "", - expectErr: true, - }, - { - name: "pool not found", - poolId: 1000000, - tokenIn: "50unibi", - tokenOutDenom: "uusdc", - respType: &sdk.TxResponse{}, - expectedCode: types.ErrPoolNotFound.ABCICode(), - expectErr: false, - }, - { - name: "token in denom not found", - poolId: 1, - tokenIn: "50foo", - tokenOutDenom: "uusdc", - respType: &sdk.TxResponse{}, - expectedCode: types.ErrTokenDenomNotFound.ABCICode(), - expectErr: false, - }, - { - name: "token out denom not found", - poolId: 1, - tokenIn: "50unibi", - tokenOutDenom: "foo", - respType: &sdk.TxResponse{}, - expectedCode: types.ErrTokenDenomNotFound.ABCICode(), - expectErr: false, - }, - { - name: "successful swap", - poolId: 1, - tokenIn: "50unibi", - tokenOutDenom: "uusdc", - respType: &sdk.TxResponse{}, - expectedCode: 0, - expectErr: false, - }, - } - - for _, tc := range testCases { - tc := tc - ctx := val.ClientCtx - - s.Run(tc.name, func() { - out, err := ExecMsgSwapAssets(s.T(), ctx, tc.poolId, s.testAccount, tc.tokenIn, tc.tokenOutDenom) - if tc.expectErr { - s.Require().Error(err) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) - } - }) - } -} - -/***************************** Convenience Methods ****************************/ - -/* -Adds tokens from val[0] to a recipient address. - -args: - - recipient: the recipient address - - tokens: the amount of tokens to transfer -*/ -func (s *IntegrationTestSuite) FundAccount(recipient sdk.Address, tokens sdk.Coins) { - val := s.network.Validators[0] - - // fund the user - _, err := banktestutil.MsgSendExec( - val.ClientCtx, - /*from=*/ val.Address, - /*to=*/ recipient, - /*amount=*/ tokens, - /*extraArgs*/ - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewInt64Coin(s.cfg.BondDenom, 10)), - ) - s.Require().NoError(err) -} - -/* -Creates a new account and returns the address. - -args: - - uid: a unique identifier to ensure duplicate accounts are not created - -ret: - - addr: the address of the new account -*/ -func (s *IntegrationTestSuite) NewAccount(uid string) (addr sdk.AccAddress) { - val := s.network.Validators[0] - - // create a new user address - info, _, err := val.ClientCtx.Keyring.NewMnemonic( - uid, - keyring.English, - sdk.FullFundraiserPath, - "iron fossil rug jazz mosquito sand kangaroo noble motor jungle job silk naive assume poverty afford twist critic start solid actual fetch flat fix", - hd.Secp256k1, - ) - s.Require().NoError(err) - - return sdk.AccAddress(info.GetPubKey().Address()) -} - -func TestIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(IntegrationTestSuite)) + suite.Run(t, NewIntegrationTestSuite(cfg)) } diff --git a/x/dex/client/testutil/suite.go b/x/dex/client/testutil/suite.go new file mode 100644 index 000000000..25d7ecbf8 --- /dev/null +++ b/x/dex/client/testutil/suite.go @@ -0,0 +1,514 @@ +package testutil + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/suite" + tmcli "github.com/tendermint/tendermint/libs/cli" + + "github.com/NibiruChain/nibiru/x/dex/client/cli" + "github.com/NibiruChain/nibiru/x/dex/types" + testutilcli "github.com/NibiruChain/nibiru/x/testutil/cli" +) + +type IntegrationTestSuite struct { + suite.Suite + + cfg testutilcli.Config + network *testutilcli.Network +} + +func NewIntegrationTestSuite(cfg testutilcli.Config) *IntegrationTestSuite { + return &IntegrationTestSuite{cfg: cfg} +} + +func (s *IntegrationTestSuite) SetupSuite() { + /* Make test skip if -short is not used: + All tests: `go test ./...` + Unit tests only: `go test ./... -short` + Integration tests only: `go test ./... -run Integration` + https://stackoverflow.com/a/41407042/13305627 */ + if testing.Short() { + s.T().Skip("skipping integration test suite") + } + + s.T().Log("setting up integration test suite") + + s.network = testutilcli.NewNetwork(s.T(), s.cfg) + _, err := s.network.WaitForHeight(1) + s.NoError(err) +} + +func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +func (s *IntegrationTestSuite) TestCreatePoolCmd_Errors() { + val := s.network.Validators[0] + + tc := []struct { + name string + tokenWeights string + initialDeposit string + expectedErr error + expectedCode uint32 + queryexpectedErr string + queryArgs []string + }{ + { + name: "create pool with insufficient funds", + tokenWeights: fmt.Sprintf("1%s, 1%s", "coin-1", "coin-2"), + initialDeposit: fmt.Sprintf("1000000000%s,10000000000%s", "coin-1", "coin-2"), + expectedCode: 5, // bankKeeper code for insufficient funds + queryexpectedErr: "pool not found", + }, + { + name: "create pool with invalid weights", + tokenWeights: fmt.Sprintf("0%s, 1%s", "coin-1", "coin-2"), + initialDeposit: fmt.Sprintf("10000%s,10000%s", "coin-1", "coin-2"), + expectedErr: types.ErrInvalidCreatePoolArgs, + queryexpectedErr: "pool not found", + }, + { + name: "create pool with deposit not matching weights", + tokenWeights: fmt.Sprintf("1%s, 1%s", "coin-1", "coin-2"), + initialDeposit: "1000foo,10000uusdc", + expectedErr: types.ErrInvalidCreatePoolArgs, + queryexpectedErr: "pool not found", + }, + } + + for _, tc := range tc { + tc := tc + + s.Run(tc.name, func() { + out, err := ExecMsgCreatePool(s.T(), val.ClientCtx, val.Address, tc.tokenWeights, tc.initialDeposit, "0.003", "0.003") + if tc.expectedErr != nil { + s.Require().ErrorIs(err, tc.expectedErr) + } else { + s.Require().NoError(err, out.String()) + + resp := &sdk.TxResponse{} + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), resp), out.String()) + + s.Require().Equal(tc.expectedCode, resp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestCreatePoolCmd() { + val := s.network.Validators[0] + + tc := []struct { + name string + tokenWeights string + initialDeposit string + }{ + { + name: "happy path", + tokenWeights: "1unibi,1uusdc", + initialDeposit: "100unibi,100uusdc", + }, + } + + for _, tc := range tc { + tc := tc + + s.Run(tc.name, func() { + out, err := ExecMsgCreatePool(s.T(), val.ClientCtx, val.Address, tc.tokenWeights, tc.initialDeposit, "0.003", "0.003") + s.Require().NoError(err, out.String()) + + resp := &sdk.TxResponse{} + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), resp), out.String()) + + s.Require().Equal(uint32(0), resp.Code, out.String()) + + // Query balance + cmd := cli.CmdTotalPoolLiquidity() + out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, []string{"1"}) + + queryResp := types.QueryTotalPoolLiquidityResponse{} + s.Require().NoError(err, out.String()) + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &queryResp), out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewJoinPoolCmd() { + val := s.network.Validators[0] + + // create a new pool + out, err := ExecMsgCreatePool( + s.T(), + val.ClientCtx, + /*owner-*/ val.Address, + /*tokenWeights=*/ fmt.Sprintf("5%s,5%s", "coin-2", "coin-3"), + /*tokenWeights=*/ fmt.Sprintf("100%s,100%s", "coin-2", "coin-3"), + /*swapFee=*/ "0.01", + /*exitFee=*/ "0.01", + ) + s.Require().NoError(err) + + poolID, err := ExtractPoolIDFromCreatePoolResponse(val.ClientCtx.Codec, out) + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + poolId uint64 + tokensIn string + expectErr bool + respType proto.Message + expectedCode uint32 + }{ + { + name: "join pool with insufficient balance", + poolId: poolID, + tokensIn: fmt.Sprintf("1000000000%s,10000000000%s", "coin-2", "coin-3"), + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 5, // bankKeeper code for insufficient funds + }, + { + name: "join pool with sufficient balance", + poolId: poolID, + tokensIn: fmt.Sprintf("100%s,100%s", "coin-2", "coin-3"), + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + ctx := val.ClientCtx + + out, err := ExecMsgJoinPool(ctx, tc.poolId, val.Address, tc.tokensIn) + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestNewExitPoolCmd() { + s.T().Skip("this test looks like it has a bug https://github.com/NibiruChain/nibiru/issues/869") + val := s.network.Validators[0] + + // create a new pool + out, err := ExecMsgCreatePool( + s.T(), + val.ClientCtx, + /*owner-*/ val.Address, + /*tokenWeights=*/ fmt.Sprintf("1%s,1%s", "coin-3", "coin-4"), + /*tokenWeights=*/ fmt.Sprintf("100%s,100%s", "coin-3", "coin-4"), + /*swapFee=*/ "0.01", + /*exitFee=*/ "0.01", + ) + s.Require().NoError(err) + + poolID, err := ExtractPoolIDFromCreatePoolResponse(val.ClientCtx.Codec, out) + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + poolId uint64 + poolSharesOut string + expectErr bool + respType proto.Message + expectedCode uint32 + expectedunibi sdk.Int + expectedOtherToken sdk.Int + }{ + { + name: "exit pool from invalid pool", + poolId: 100, + poolSharesOut: "100nibiru/pool/100", + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 1, // dex.types.ErrNonExistingPool + expectedunibi: sdk.NewInt(-10), + expectedOtherToken: sdk.NewInt(0), + }, + { + name: "exit pool for too many shares", + poolId: poolID, + poolSharesOut: fmt.Sprintf("1001000000000000000000nibiru/pool/%d", poolID), + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 1, + expectedunibi: sdk.NewInt(-10), + expectedOtherToken: sdk.NewInt(0), + }, + { + name: "exit pool for zero shares", + poolId: poolID, + poolSharesOut: fmt.Sprintf("0nibiru/pool/%d", poolID), + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 1, + expectedunibi: sdk.NewInt(-10), + expectedOtherToken: sdk.NewInt(0), + }, + { // Looks with a bug + name: "exit pool with sufficient balance", + poolId: poolID, + poolSharesOut: fmt.Sprintf("100000000000000000000nibiru/pool/%d", poolID), + expectErr: false, + respType: &sdk.TxResponse{}, + expectedCode: 0, + expectedunibi: sdk.NewInt(100 - 10 - 1), // Received unibi minus 10unibi tx fee minus 1 exit pool fee + expectedOtherToken: sdk.NewInt(100 - 1), // Received uusdc minus 1 exit pool fee + }, + } + + for _, tc := range testCases { + tc := tc + ctx := val.ClientCtx + + s.Run(tc.name, func() { + // Get original balance + resp, err := banktestutil.QueryBalancesExec(ctx, val.Address) + s.Require().NoError(err) + var originalBalance banktypes.QueryAllBalancesResponse + s.Require().NoError(ctx.Codec.UnmarshalJSON(resp.Bytes(), &originalBalance)) + + out, err := ExecMsgExitPool(ctx, tc.poolId, val.Address, tc.poolSharesOut) + + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + + // Ensure balance is ok + resp, err := banktestutil.QueryBalancesExec(ctx, val.Address) + s.Require().NoError(err) + var finalBalance banktypes.QueryAllBalancesResponse + s.Require().NoError(ctx.Codec.UnmarshalJSON(resp.Bytes(), &finalBalance)) + + s.Require().Equal( + originalBalance.Balances.AmountOf("uusdc").Add(tc.expectedOtherToken), + finalBalance.Balances.AmountOf("uusdc"), + ) + s.Require().Equal( + originalBalance.Balances.AmountOf("unibi").Add(tc.expectedunibi), + finalBalance.Balances.AmountOf("unibi"), + ) + } + }) + } +} + +func (s *IntegrationTestSuite) TestGetCmdTotalLiquidity() { + val := s.network.Validators[0] + + testCases := []struct { + name string + args []string + expectErr bool + }{ + { + "query total liquidity", // nibid query dex total-liquidity + []string{ + fmt.Sprintf("--%s=%s", tmcli.OutputFlag, "json"), + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := cli.CmdTotalLiquidity() + clientCtx := val.ClientCtx + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Error(err) + } else { + resp := types.QueryTotalLiquidityResponse{} + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestSwapAssets() { + val := s.network.Validators[0] + + // create a new pool + out, err := ExecMsgCreatePool( + s.T(), + val.ClientCtx, + /*owner-*/ val.Address, + /*tokenWeights=*/ fmt.Sprintf("1%s,1%s", "coin-4", "coin-5"), + /*tokenWeights=*/ fmt.Sprintf("100%s,100%s", "coin-4", "coin-5"), + /*swapFee=*/ "0.01", + /*exitFee=*/ "0.01", + ) + s.Require().NoError(err) + + poolID, err := ExtractPoolIDFromCreatePoolResponse(val.ClientCtx.Codec, out) + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + poolId uint64 + tokenIn string + tokenOutDenom string + respType proto.Message + expectedCode uint32 + expectErr bool + }{ + { + name: "zero pool id", + poolId: 0, + tokenIn: "50unibi", + tokenOutDenom: "uusdc", + expectErr: true, + }, + { + name: "invalid token in", + poolId: poolID, + tokenIn: "0coin-4", + tokenOutDenom: "uusdc", + expectErr: true, + }, + { + name: "invalid token out denom", + poolId: poolID, + tokenIn: "50coin-4", + tokenOutDenom: "", + expectErr: true, + }, + { + name: "pool not found", + poolId: 1000000, + tokenIn: "50unibi", + tokenOutDenom: "uusdc", + respType: &sdk.TxResponse{}, + expectedCode: types.ErrPoolNotFound.ABCICode(), + expectErr: false, + }, + { + name: "token in denom not found", + poolId: poolID, + tokenIn: "50foo", + tokenOutDenom: "coin-5", + respType: &sdk.TxResponse{}, + expectedCode: types.ErrTokenDenomNotFound.ABCICode(), + expectErr: false, + }, + { + name: "token out denom not found", + poolId: poolID, + tokenIn: "50coin-4", + tokenOutDenom: "foo", + respType: &sdk.TxResponse{}, + expectedCode: types.ErrTokenDenomNotFound.ABCICode(), + expectErr: false, + }, + { + name: "successful swap", + poolId: poolID, + tokenIn: "50coin-4", + tokenOutDenom: "coin-5", + respType: &sdk.TxResponse{}, + expectedCode: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + tc := tc + ctx := val.ClientCtx + + s.Run(tc.name, func() { + out, err := ExecMsgSwapAssets(ctx, tc.poolId, val.Address, tc.tokenIn, tc.tokenOutDenom) + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(ctx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +/***************************** Convenience Methods ****************************/ + +/* +Adds tokens from val[0] to a recipient address. + +args: + - recipient: the recipient address + - tokens: the amount of tokens to transfer +*/ +func (s *IntegrationTestSuite) FundAccount(recipient sdk.Address, tokens sdk.Coins) { + val := s.network.Validators[0] + + // fund the user + _, err := banktestutil.MsgSendExec( + val.ClientCtx, + /*from=*/ val.Address, + /*to=*/ recipient, + /*amount=*/ tokens, + /*extraArgs*/ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewInt64Coin(s.cfg.BondDenom, 10)), + ) + s.Require().NoError(err) +} + +/* +Creates a new account and returns the address. + +args: + - uid: a unique identifier to ensure duplicate accounts are not created + +ret: + - addr: the address of the new account +*/ +func (s *IntegrationTestSuite) NewAccount(uid string) (addr sdk.AccAddress) { + val := s.network.Validators[0] + + // create a new user address + info, _, err := val.ClientCtx.Keyring.NewMnemonic( + uid, + keyring.English, + sdk.FullFundraiserPath, + "iron fossil rug jazz mosquito sand kangaroo noble motor jungle job silk naive assume poverty afford twist critic start solid actual fetch flat fix", + hd.Secp256k1, + ) + s.Require().NoError(err) + + return sdk.AccAddress(info.GetPubKey().Address()) +} diff --git a/x/dex/client/testutil/cli_helpers.go b/x/dex/client/testutil/test_helpers.go similarity index 53% rename from x/dex/client/testutil/cli_helpers.go rename to x/dex/client/testutil/test_helpers.go index cd93a2029..a9767ae76 100644 --- a/x/dex/client/testutil/cli_helpers.go +++ b/x/dex/client/testutil/test_helpers.go @@ -1,17 +1,22 @@ -package cli_test +package testutil import ( + "encoding/hex" "fmt" "testing" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/testutil" clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/simapp" "github.com/NibiruChain/nibiru/x/common" - dexcli "github.com/NibiruChain/nibiru/x/dex/client/cli" + "github.com/NibiruChain/nibiru/x/dex/client/cli" + "github.com/NibiruChain/nibiru/x/dex/types" ) // commonArgs is args for CLI test commands. @@ -51,7 +56,7 @@ func ExecMsgCreatePool( ) args = append(args, - fmt.Sprintf("--%s=%s", dexcli.FlagPoolFile, jsonFile.Name()), + fmt.Sprintf("--%s=%s", cli.FlagPoolFile, jsonFile.Name()), fmt.Sprintf("--%s=%s", flags.FlagFrom, owner.String()), fmt.Sprintf("--%s=%d", flags.FlagGas, 300000), ) @@ -59,12 +64,11 @@ func ExecMsgCreatePool( args = append(args, commonArgs...) args = append(args, extraArgs...) - return clitestutil.ExecTestCLICmd(clientCtx, dexcli.CmdCreatePool(), args) + return clitestutil.ExecTestCLICmd(clientCtx, cli.CmdCreatePool(), args) } // ExecMsgJoinPool broadcast a join pool message. func ExecMsgJoinPool( - t *testing.T, clientCtx client.Context, poolId uint64, sender fmt.Stringer, @@ -72,8 +76,8 @@ func ExecMsgJoinPool( extraArgs ...string, ) (testutil.BufferWriter, error) { args := []string{ - fmt.Sprintf("--%s=%d", dexcli.FlagPoolId, poolId), - fmt.Sprintf("--%s=%s", dexcli.FlagTokensIn, tokensIn), + fmt.Sprintf("--%s=%d", cli.FlagPoolId, poolId), + fmt.Sprintf("--%s=%s", cli.FlagTokensIn, tokensIn), fmt.Sprintf("--%s=%s", flags.FlagFrom, sender.String()), fmt.Sprintf("--%s=%d", flags.FlagGas, 300000), } @@ -81,12 +85,11 @@ func ExecMsgJoinPool( args = append(args, commonArgs...) args = append(args, extraArgs...) - return clitestutil.ExecTestCLICmd(clientCtx, dexcli.CmdJoinPool(), args) + return clitestutil.ExecTestCLICmd(clientCtx, cli.CmdJoinPool(), args) } // ExecMsgExitPool broadcast an exit pool message. func ExecMsgExitPool( - t *testing.T, clientCtx client.Context, poolId uint64, sender fmt.Stringer, @@ -94,8 +97,8 @@ func ExecMsgExitPool( extraArgs ...string, ) (testutil.BufferWriter, error) { args := []string{ - fmt.Sprintf("--%s=%d", dexcli.FlagPoolId, poolId), - fmt.Sprintf("--%s=%s", dexcli.FlagPoolSharesOut, poolSharesOut), + fmt.Sprintf("--%s=%d", cli.FlagPoolId, poolId), + fmt.Sprintf("--%s=%s", cli.FlagPoolSharesOut, poolSharesOut), fmt.Sprintf("--%s=%s", flags.FlagFrom, sender.String()), fmt.Sprintf("--%s=%d", flags.FlagGas, 300000), } @@ -103,12 +106,11 @@ func ExecMsgExitPool( args = append(args, commonArgs...) args = append(args, extraArgs...) - return clitestutil.ExecTestCLICmd(clientCtx, dexcli.CmdExitPool(), args) + return clitestutil.ExecTestCLICmd(clientCtx, cli.CmdExitPool(), args) } // ExecMsgSwapAssets broadcast a swap assets message. func ExecMsgSwapAssets( - t *testing.T, clientCtx client.Context, poolId uint64, sender fmt.Stringer, @@ -117,9 +119,9 @@ func ExecMsgSwapAssets( extraArgs ...string, ) (testutil.BufferWriter, error) { args := []string{ - fmt.Sprintf("--%s=%d", dexcli.FlagPoolId, poolId), - fmt.Sprintf("--%s=%s", dexcli.FlagTokenIn, tokenIn), - fmt.Sprintf("--%s=%s", dexcli.FlagTokenOutDenom, tokenOutDenom), + fmt.Sprintf("--%s=%d", cli.FlagPoolId, poolId), + fmt.Sprintf("--%s=%s", cli.FlagTokenIn, tokenIn), + fmt.Sprintf("--%s=%s", cli.FlagTokenOutDenom, tokenOutDenom), fmt.Sprintf("--%s=%s", flags.FlagFrom, sender.String()), fmt.Sprintf("--%s=%d", flags.FlagGas, 300_000), } @@ -127,5 +129,53 @@ func ExecMsgSwapAssets( args = append(args, commonArgs...) args = append(args, extraArgs...) - return clitestutil.ExecTestCLICmd(clientCtx, dexcli.CmdSwapAssets(), args) + return clitestutil.ExecTestCLICmd(clientCtx, cli.CmdSwapAssets(), args) +} + +// WhitelistGenesisAssets given a simapp.GenesisState includes the whitelisted assets into Dex Whitelisted assets. +func WhitelistGenesisAssets(state simapp.GenesisState, assets []string) simapp.GenesisState { + encConfig := app.MakeTestEncodingConfig() + + jsonState := state[types.ModuleName] + + var genesis types.GenesisState + encConfig.Marshaler.MustUnmarshalJSON(jsonState, &genesis) + genesis.Params.WhitelistedAsset = assets + + json, _ := encConfig.Marshaler.MarshalJSON(&genesis) + state[types.ModuleName] = json + + return state +} + +// ExtractPoolIDFromCreatePoolResponse extracts the created PoolID from a MsgCreatePool command. +func ExtractPoolIDFromCreatePoolResponse(codec codec.Codec, out testutil.BufferWriter) (uint64, error) { + resp := &sdk.TxResponse{} + err := codec.UnmarshalJSON(out.Bytes(), resp) + if err != nil { + return 0, err + } + + decodedResult, err := hex.DecodeString(resp.Data) + if err != nil { + return 0, err + } + + respData := sdk.TxMsgData{} + err = codec.Unmarshal(decodedResult, &respData) + if err != nil { + return 0, err + } + + if len(respData.Data) < 1 { + return 0, fmt.Errorf("invalid response") + } + + var createPoolResponse types.MsgCreatePoolResponse + err = codec.Unmarshal(respData.Data[0].Data, &createPoolResponse) + if err != nil { + return 0, err + } + + return createPoolResponse.PoolId, nil } diff --git a/x/dex/keeper/keeper.go b/x/dex/keeper/keeper.go index 92795d186..79478f8df 100644 --- a/x/dex/keeper/keeper.go +++ b/x/dex/keeper/keeper.go @@ -529,6 +529,7 @@ func (k Keeper) ExitPool( poolSharesOut.Denom, ) } + if poolSharesOut.Amount.GT(pool.TotalShares.Amount) || poolSharesOut.Amount.LTE(sdk.ZeroInt()) { return sdk.Coins{}, errors.New("invalid number of pool shares") diff --git a/x/testutil/cli/network.go b/x/testutil/cli/network.go index 00f6ed6eb..7230a7944 100644 --- a/x/testutil/cli/network.go +++ b/x/testutil/cli/network.go @@ -64,7 +64,7 @@ type Config struct { TxConfig client.TxConfig AccountRetriever client.AccountRetriever AppConstructor AppConstructor // the ABCI application constructor - GenesisState map[string]json.RawMessage // custom gensis state to provide + GenesisState map[string]json.RawMessage // custom genesis state to provide TimeoutCommit time.Duration // the consensus commitment timeout ChainID string // the network chain-id NumValidators int // the total number of validators to create and bond