diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ef98e915..42c91d80b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +* Add CLI commands for the exchange module endpoints and queries [#1701](https://github.com/provenance-io/provenance/issues/1701). + ### Improvements * Add upgrade handler for 1.18 [#1756](https://github.com/provenance-io/provenance/pull/1756). diff --git a/x/exchange/client/cli/cli_test.go b/x/exchange/client/cli/cli_test.go new file mode 100644 index 0000000000..f23410121d --- /dev/null +++ b/x/exchange/client/cli/cli_test.go @@ -0,0 +1,1015 @@ +package cli_test + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/gogo/protobuf/proto" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + testnet "github.com/cosmos/cosmos-sdk/testutil/network" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/internal/antewrapper" + "github.com/provenance-io/provenance/internal/pioconfig" + "github.com/provenance-io/provenance/testutil" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" + "github.com/provenance-io/provenance/x/hold" +) + +type CmdTestSuite struct { + suite.Suite + + cfg testnet.Config + testnet *testnet.Network + keyring keyring.Keyring + keyringDir string + accountAddrs []sdk.AccAddress + + addr0 sdk.AccAddress + addr1 sdk.AccAddress + addr2 sdk.AccAddress + addr3 sdk.AccAddress + addr4 sdk.AccAddress + addr5 sdk.AccAddress + addr6 sdk.AccAddress + addr7 sdk.AccAddress + addr8 sdk.AccAddress + addr9 sdk.AccAddress + + addrNameLookup map[string]string +} + +func TestCmdTestSuite(t *testing.T) { + suite.Run(t, new(CmdTestSuite)) +} + +func (s *CmdTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + pioconfig.SetProvenanceConfig("", 0) + s.cfg = testutil.DefaultTestNetworkConfig() + s.cfg.NumValidators = 1 + s.cfg.ChainID = antewrapper.SimAppChainID + s.cfg.TimeoutCommit = 500 * time.Millisecond + + s.generateAccountsWithKeyring(10) + s.addr0 = s.accountAddrs[0] + s.addr1 = s.accountAddrs[1] + s.addr2 = s.accountAddrs[2] + s.addr3 = s.accountAddrs[3] + s.addr4 = s.accountAddrs[4] + s.addr5 = s.accountAddrs[5] + s.addr6 = s.accountAddrs[6] + s.addr7 = s.accountAddrs[7] + s.addr8 = s.accountAddrs[8] + s.addr9 = s.accountAddrs[9] + s.addrNameLookup = map[string]string{ + s.addr0.String(): "addr0", + s.addr1.String(): "addr1", + s.addr2.String(): "addr2", + s.addr3.String(): "addr3", + s.addr4.String(): "addr4", + s.addr5.String(): "addr5", + s.addr6.String(): "addr6", + s.addr7.String(): "addr7", + s.addr8.String(): "addr8", + s.addr9.String(): "addr9", + cli.AuthorityAddr.String(): "authorityAddr", + } + + // Add accounts to auth gen state. + var authGen authtypes.GenesisState + err := s.cfg.Codec.UnmarshalJSON(s.cfg.GenesisState[authtypes.ModuleName], &authGen) + s.Require().NoError(err, "UnmarshalJSON auth gen state") + genAccs := make(authtypes.GenesisAccounts, len(s.accountAddrs)) + for i, addr := range s.accountAddrs { + genAccs[i] = authtypes.NewBaseAccount(addr, nil, 0, 1) + } + newAccounts, err := authtypes.PackAccounts(genAccs) + s.Require().NoError(err, "PackAccounts") + authGen.Accounts = append(authGen.Accounts, newAccounts...) + s.cfg.GenesisState[authtypes.ModuleName], err = s.cfg.Codec.MarshalJSON(&authGen) + s.Require().NoError(err, "MarshalJSON auth gen state") + + // Add some markets to the exchange gen state. + var exchangeGen exchange.GenesisState + err = s.cfg.Codec.UnmarshalJSON(s.cfg.GenesisState[exchange.ModuleName], &exchangeGen) + s.Require().NoError(err, "UnmarshalJSON exchange gen state") + exchangeGen.Params = exchange.DefaultParams() + exchangeGen.Markets = append(exchangeGen.Markets, + exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "Market Three", + Description: "The third market (or is it?). It only has ask/seller fees.", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 10)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 50)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 100), Fee: sdk.NewInt64Coin("peach", 1)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + {Address: s.addr2.String(), Permissions: exchange.AllPermissions()}, + {Address: s.addr3.String(), Permissions: []exchange.Permission{exchange.Permission_cancel, exchange.Permission_attributes}}, + }, + }, + exchange.Market{ + MarketId: 5, + MarketDetails: exchange.MarketDetails{ + Name: "Market Five", + Description: "Market the Fifth. It only has bid/buyer fees.", + }, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 10)}, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 50)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 100), Fee: sdk.NewInt64Coin("peach", 1)}, + {Price: sdk.NewInt64Coin("peach", 100), Fee: sdk.NewInt64Coin(s.cfg.BondDenom, 3)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + }, + }, + // Do not make a market 419, lots of tests expect it to not exist. + exchange.Market{ + // The orders in this market are for the orders queries. + // Don't use it in other unit tests (e.g. order creation or settlement). + MarketId: 420, + MarketDetails: exchange.MarketDetails{ + Name: "THE Market", + Description: "It's coming; you know it. It has all the fees.", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 20)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 25)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 100)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 75), Fee: sdk.NewInt64Coin("peach", 1)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("peach", 105)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 50), Fee: sdk.NewInt64Coin("peach", 1)}, + {Price: sdk.NewInt64Coin("peach", 50), Fee: sdk.NewInt64Coin(s.cfg.BondDenom, 3)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr1.String(), Permissions: exchange.AllPermissions()}, + }, + ReqAttrCreateAsk: []string{"seller.kyc"}, + ReqAttrCreateBid: []string{"buyer.kyc"}, + }, + exchange.Market{ + // This market has an invalid setup. Don't mess with it. + MarketId: 421, + MarketDetails: exchange.MarketDetails{Name: "Broken"}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 55), Fee: sdk.NewInt64Coin("peach", 1)}, + }, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("peach", 56), Fee: sdk.NewInt64Coin("peach", 1)}, + {Price: sdk.NewInt64Coin("plum", 57), Fee: sdk.NewInt64Coin("plum", 1)}, + }, + }, + ) + toHold := make(map[string]sdk.Coins) + exchangeGen.Orders = make([]exchange.Order, 60) + for i := range exchangeGen.Orders { + order := s.makeInitialOrder(uint64(i + 1)) + exchangeGen.Orders[i] = *order + toHold[order.GetOwner()] = toHold[order.GetOwner()].Add(order.GetHoldAmount()...) + } + exchangeGen.LastOrderId = uint64(100) + s.cfg.GenesisState[exchange.ModuleName], err = s.cfg.Codec.MarshalJSON(&exchangeGen) + s.Require().NoError(err, "MarshalJSON exchange gen state") + + // Create all the needed holds. + var holdGen hold.GenesisState + err = s.cfg.Codec.UnmarshalJSON(s.cfg.GenesisState[hold.ModuleName], &holdGen) + s.Require().NoError(err, "UnmarshalJSON hold gen state") + for _, addr := range s.accountAddrs { + holdGen.Holds = append(holdGen.Holds, &hold.AccountHold{ + Address: addr.String(), + Amount: toHold[addr.String()], + }) + } + s.cfg.GenesisState[hold.ModuleName], err = s.cfg.Codec.MarshalJSON(&holdGen) + s.Require().NoError(err, "MarshalJSON hold gen state") + + // Add balances to bank gen state. + // Any initial holds for an account are added to this so that + // this is what's available to each at the start of the unit tests. + balance := sdk.NewCoins( + sdk.NewInt64Coin(s.cfg.BondDenom, 1_000_000_000), + sdk.NewInt64Coin("acorn", 1_000_000_000), + sdk.NewInt64Coin("apple", 1_000_000_000), + sdk.NewInt64Coin("peach", 1_000_000_000), + ) + var bankGen banktypes.GenesisState + err = s.cfg.Codec.UnmarshalJSON(s.cfg.GenesisState[banktypes.ModuleName], &bankGen) + s.Require().NoError(err, "UnmarshalJSON bank gen state") + for _, addr := range s.accountAddrs { + bal := balance.Add(toHold[addr.String()]...) + bankGen.Balances = append(bankGen.Balances, banktypes.Balance{Address: addr.String(), Coins: bal}) + } + s.cfg.GenesisState[banktypes.ModuleName], err = s.cfg.Codec.MarshalJSON(&bankGen) + s.Require().NoError(err, "MarshalJSON bank gen state") + + // And fire it all up!! + s.testnet, err = testnet.New(s.T(), s.T().TempDir(), s.cfg) + s.Require().NoError(err, "testnet.New(...)") + + _, err = s.testnet.WaitForHeight(1) + s.Require().NoError(err, "s.testnet.WaitForHeight(1)") +} + +func (s *CmdTestSuite) TearDownSuite() { + testutil.CleanUp(s.testnet, s.T()) +} + +// generateAccountsWithKeyring creates a keyring and adds a number of keys to it. +// The s.keyringDir, s.keyring, and s.accountAddrs are all set in here. +// The getClientCtx function returns a context that knows about this keyring. +func (s *CmdTestSuite) generateAccountsWithKeyring(number int) { + path := hd.CreateHDPath(118, 0, 0).String() + s.keyringDir = s.T().TempDir() + var err error + s.keyring, err = keyring.New(s.T().Name(), "test", s.keyringDir, nil, s.cfg.Codec) + s.Require().NoError(err, "keyring.New(...)") + + s.accountAddrs = make([]sdk.AccAddress, number) + for i := range s.accountAddrs { + keyId := fmt.Sprintf("test_key_%v", i) + var info *keyring.Record + info, _, err = s.keyring.NewMnemonic(keyId, keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + s.Require().NoError(err, "[%d] s.keyring.NewMnemonic(...)", i) + s.accountAddrs[i], err = info.GetAddress() + s.Require().NoError(err, "[%d] getting keyring address", i) + } +} + +// makeInitialOrder makes an order using the order id for various aspects. +func (s *CmdTestSuite) makeInitialOrder(orderID uint64) *exchange.Order { + addr := s.accountAddrs[int(orderID)%len(s.accountAddrs)] + assetDenom := "apple" + if orderID%7 <= 2 { + assetDenom = "acorn" + } + assets := sdk.NewInt64Coin(assetDenom, int64(orderID*100)) + price := sdk.NewInt64Coin("peach", int64(orderID*orderID*10)) + partial := orderID%2 == 0 + externalID := fmt.Sprintf("my-id-%d", orderID) + order := exchange.NewOrder(orderID) + switch orderID % 6 { + case 0, 1, 4: + order.WithAsk(&exchange.AskOrder{ + MarketId: 420, + Seller: addr.String(), + Assets: assets, + Price: price, + ExternalId: externalID, + AllowPartial: partial, + }) + case 2, 3, 5: + order.WithBid(&exchange.BidOrder{ + MarketId: 420, + Buyer: addr.String(), + Assets: assets, + Price: price, + ExternalId: externalID, + AllowPartial: partial, + }) + } + return order +} + +// getClientCtx get a client context that knows about the suite's keyring. +func (s *CmdTestSuite) getClientCtx() client.Context { + return s.testnet.Validators[0].ClientCtx. + WithKeyringDir(s.keyringDir). + WithKeyring(s.keyring) +} + +// getAddrName tries to get the variable name (in this suite) of the provided address. +func (s *CmdTestSuite) getAddrName(addr string) string { + if rv, found := s.addrNameLookup[addr]; found { + return rv + } + return addr +} + +// txCmdTestCase is a test case for a TX command. +type txCmdTestCase struct { + // name is a name for this test case. + name string + // preRun is a function that is run first. + // It should return any arguments to append to the args and a function that will + // run any follow-up checks to do after the command is run. + preRun func() ([]string, func(*sdk.TxResponse)) + // args are the arguments to provide to the command. + args []string + // expInErr are strings to expect in an error from the cmd. + // Errors that come from the endpoint will not be here; use expInRawLog for those. + expInErr []string + // expInRawLog are strings to expect in the TxResponse.RawLog. + expInRawLog []string + // expectedCode is the code expected from the Tx. + expectedCode uint32 +} + +// RunTxCmdTestCase runs a txCmdTestCase by executing the command and checking the result. +func (s *CmdTestSuite) runTxCmdTestCase(tc txCmdTestCase) { + s.T().Helper() + var extraArgs []string + var followup func(*sdk.TxResponse) + var preRunFailed bool + if tc.preRun != nil { + s.Run("pre-run: "+tc.name, func() { + preRunFailed = true + extraArgs, followup = tc.preRun() + preRunFailed = s.T().Failed() + }) + } + + cmd := cli.CmdTx() + + args := append(tc.args, extraArgs...) + args = append(args, + "--"+flags.FlagGas, "250000", + "--"+flags.FlagFees, s.bondCoins(10).String(), + "--"+flags.FlagBroadcastMode, flags.BroadcastBlock, + "--"+flags.FlagSkipConfirmation, + ) + + var txResponse *sdk.TxResponse + var cmdFailed bool + testRunner := func() { + if preRunFailed { + s.T().Skip("Skipping execution due to pre-run failure.") + } + + cmdName := cmd.Name() + var outBz []byte + defer func() { + if s.T().Failed() { + s.T().Logf("Command: %s\nArgs: %q\nOutput\n%s", cmdName, args, string(outBz)) + cmdFailed = true + } + }() + + clientCtx := s.getClientCtx() + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args) + outBz = out.Bytes() + + s.assertErrorContents(err, tc.expInErr, "ExecTestCLICmd error") + for _, exp := range tc.expInErr { + s.Assert().Contains(string(outBz), exp, "command output should contain:\n%q", exp) + } + + if len(tc.expInErr) == 0 && err == nil { + var resp sdk.TxResponse + err = clientCtx.Codec.UnmarshalJSON(outBz, &resp) + if s.Assert().NoError(err, "UnmarshalJSON(command output) error") { + txResponse = &resp + s.Assert().Equal(int(tc.expectedCode), int(resp.Code), "response code") + for _, exp := range tc.expInRawLog { + s.Assert().Contains(resp.RawLog, exp, "TxResponse.RawLog should contain:\n%q", exp) + } + } + } + } + + if tc.preRun != nil { + s.Run("execute: "+tc.name, testRunner) + } else { + testRunner() + } + + if followup != nil { + s.Run("followup: "+tc.name, func() { + if preRunFailed { + s.T().Skip("Skipping followup due to pre-run failure.") + } + if cmdFailed { + s.T().Skip("Skipping followup due to failure with command.") + } + if s.Assert().NotNil(txResponse, "the TxResponse from the command output") { + followup(txResponse) + } + }) + } +} + +// queryCmdTestCase is a test case of a query command. +type queryCmdTestCase struct { + // name is a name for this test case. + name string + // args are the arguments to provide to the command. + args []string + // expInErr are strings to expect in an error message (and output). + expInErr []string + // expInOut are strings to expect in the output. + expInOut []string + // expOut is the expected full output. Leave empty to skip this check. + expOut string +} + +// RunQueryCmdTestCase runs a queryCmdTestCase by executing the command and checking the result. +func (s *CmdTestSuite) runQueryCmdTestCase(tc queryCmdTestCase) { + s.T().Helper() + cmd := cli.CmdQuery() + + cmdName := cmd.Name() + var outStr string + defer func() { + if s.T().Failed() { + s.T().Logf("Command: %s\nArgs: %q\nOutput\n%s", cmdName, tc.args, outStr) + } + }() + + clientCtx := s.getClientCtx() + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + outStr = out.String() + + s.assertErrorContents(err, tc.expInErr, "ExecTestCLICmd error") + for _, exp := range tc.expInErr { + if !s.Assert().Contains(outStr, exp, "command output (error)") { + s.T().Logf("Not found: %q", exp) + } + } + + for _, exp := range tc.expInOut { + if !s.Assert().Contains(outStr, exp, "command output") { + s.T().Logf("Not found: %q", exp) + } + } + + if len(tc.expOut) > 0 { + s.Assert().Equal(tc.expOut, outStr, "command output string") + } +} + +// getEventAttribute finds the value of an attribute in an event. +// Returns an error if the value is empty, the attribute doesn't exist, or the event doesn't exist. +func (s *CmdTestSuite) getEventAttribute(events []abci.Event, eventType, attribute string) (string, error) { + for _, event := range events { + if event.Type == eventType { + for _, attr := range event.Attributes { + if string(attr.Key) == attribute { + val := strings.Trim(string(attr.Value), `"`) + if len(val) > 0 { + return val, nil + } + return "", fmt.Errorf("the %s.%s value is empty", eventType, attribute) + } + } + return "", fmt.Errorf("no %s attribute found in %s", attribute, eventType) + } + } + return "", fmt.Errorf("no %s found", eventType) +} + +// findNewOrderID gets the order id from the EventOrderCreated event. +func (s *CmdTestSuite) findNewOrderID(resp *sdk.TxResponse) (string, error) { + return s.getEventAttribute(resp.Events, "provenance.exchange.v1.EventOrderCreated", "order_id") +} + +// assertOrder uses the GetOrder query to look up an order and make sure it equals the one provided. +// If the provided order is nil, ensures the query returns an order not found error. +func (s *CmdTestSuite) assertGetOrder(orderID string, order *exchange.Order) (okay bool) { + s.T().Helper() + if !s.Assert().NotEmpty(orderID, "order id") { + return false + } + + var expInErr []string + if order == nil { + expInErr = append(expInErr, fmt.Sprintf("order %s not found", orderID)) + } + + var getOrderOutBz []byte + getOrderArgs := []string{orderID, "--output", "json"} + defer func() { + if !okay { + s.T().Logf("Query GetOrder %s output:\n%s", getOrderArgs, string(getOrderOutBz)) + } + }() + + clientCtx := s.getClientCtx() + getOrderCmd := cli.CmdQueryGetOrder() + getOrderOutBW, err := clitestutil.ExecTestCLICmd(clientCtx, getOrderCmd, getOrderArgs) + getOrderOutBz = getOrderOutBW.Bytes() + if !s.assertErrorContents(err, expInErr, "ExecTestCLICmd GetOrder %s error", orderID) { + return false + } + + if order == nil { + return true + } + + var resp exchange.QueryGetOrderResponse + err = clientCtx.Codec.UnmarshalJSON(getOrderOutBz, &resp) + if !s.Assert().NoError(err, "UnmarshalJSON on GetOrder %s response", orderID) { + return false + } + return s.Assert().Equal(order, resp.Order, "order %s", orderID) +} + +// getOrderFollowup returns a follow-up function that looks up an order and makes sure it's the one provided. +func (s *CmdTestSuite) getOrderFollowup(orderID string, order *exchange.Order) func(*sdk.TxResponse) { + return func(*sdk.TxResponse) { + if order != nil { + order.OrderId = s.asOrderID(orderID) + } + s.assertGetOrder(orderID, order) + } +} + +// createOrderFollowup returns a followup function that identifies the new order id, looks it up, +// and makes sure it is as expected. +func (s *CmdTestSuite) createOrderFollowup(order *exchange.Order) func(*sdk.TxResponse) { + return func(resp *sdk.TxResponse) { + orderID, err := s.findNewOrderID(resp) + if s.Assert().NoError(err, "finding new order id") { + order.OrderId = s.asOrderID(orderID) + s.assertGetOrder(orderID, order) + } + } +} + +// getMarket executes a query to get the given market. +func (s *CmdTestSuite) getMarket(marketID string) *exchange.Market { + s.T().Helper() + if !s.Assert().NotEmpty(marketID, "market id") { + return nil + } + + okay := false + var outBz []byte + args := []string{marketID, "--output", "json"} + defer func() { + if !okay { + s.T().Logf("Query GetMarket\nArgs: %q\nOutput:\n%s", args, string(outBz)) + } + }() + + clientCtx := s.getClientCtx() + cmd := cli.CmdQueryGetMarket() + outBW, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args) + outBz = outBW.Bytes() + + s.Require().NoError(err, "ExecTestCLICmd error") + + var resp exchange.QueryGetMarketResponse + err = clientCtx.Codec.UnmarshalJSON(outBz, &resp) + s.Require().NoError(err, "UnmarshalJSON on GetMarket %s response", marketID) + s.Require().NotNil(resp.Market, "GetMarket %s response .Market", marketID) + okay = true + return resp.Market +} + +// getMarketFollowup returns a follow-up function that asserts that the existing market is as expected. +func (s *CmdTestSuite) getMarketFollowup(marketID string, expected *exchange.Market) func(*sdk.TxResponse) { + return func(_ *sdk.TxResponse) { + actual := s.getMarket(marketID) + s.Assert().Equal(expected, actual, "market %s", marketID) + } +} + +// findNewProposalID gets the proposal id from the submit_proposal event. +func (s *CmdTestSuite) findNewProposalID(resp *sdk.TxResponse) (string, error) { + return s.getEventAttribute(resp.Events, "submit_proposal", "proposal_id") +} + +// AssertGovPropMsg queries for the given proposal and makes sure it's got just the provided Msg. +func (s *CmdTestSuite) assertGovPropMsg(propID string, msg sdk.Msg) bool { + s.T().Helper() + if msg == nil { + return true + } + + if !s.Assert().NotEmpty(propID, "proposal id") { + return false + } + expPropMsgAny, err := codectypes.NewAnyWithValue(msg) + if !s.Assert().NoError(err, "NewAnyWithValue(%T)", msg) { + return false + } + + clientCtx := s.getClientCtx() + getPropCmd := govcli.GetCmdQueryProposal() + propOutBW, err := clitestutil.ExecTestCLICmd(clientCtx, getPropCmd, []string{propID, "--output", "json"}) + propOutBz := propOutBW.Bytes() + s.T().Logf("Query proposal %s output:\n%s", propID, string(propOutBz)) + if !s.Assert().NoError(err, "GetCmdQueryProposal %s error", propID) { + return false + } + + var prop govv1.Proposal + err = clientCtx.Codec.UnmarshalJSON(propOutBz, &prop) + if !s.Assert().NoError(err, "UnmarshalJSON on proposal %s response", propID) { + return false + } + if !s.Assert().Len(prop.Messages, 1, "number of messages in proposal %s", propID) { + return false + } + return s.Assert().Equal(expPropMsgAny, prop.Messages[0], "the message in proposal %s", propID) +} + +// govPropFollowup returns a followup function that identifies the new proposal id, looks it up, +// and makes sure it's got the provided msg. +func (s *CmdTestSuite) govPropFollowup(msg sdk.Msg) func(*sdk.TxResponse) { + return func(resp *sdk.TxResponse) { + propID, err := s.findNewProposalID(resp) + if s.Assert().NoError(err, "finding new proposal id") { + s.assertGovPropMsg(propID, msg) + } + } +} + +// assertErrorContents is a wrapper for assertions.AssertErrorContents using this suite's T(). +func (s *CmdTestSuite) assertErrorContents(theError error, contains []string, msgAndArgs ...interface{}) bool { + s.T().Helper() + return assertions.AssertErrorContents(s.T(), theError, contains, msgAndArgs...) +} + +// bondCoins returns a Coins with just an entry with the bond denom and the provided amount. +func (s *CmdTestSuite) bondCoins(amt int64) sdk.Coins { + return sdk.NewCoins(sdk.NewInt64Coin(s.cfg.BondDenom, amt)) +} + +// adjustBalance creates a new Balance with the order owner's Address and a Coins that's +// the result of the order and fees applied to the provided current balance. +func (s *CmdTestSuite) adjustBalance(curBal sdk.Coins, order *exchange.Order, creationFees ...sdk.Coin) banktypes.Balance { + rv := banktypes.Balance{ + Address: order.GetOwner(), + } + + price := order.GetPrice() + assets := order.GetAssets() + var hasNeg bool + if order.IsAskOrder() { + rv.Coins, hasNeg = curBal.Add(price).SafeSub(assets) + s.Require().False(hasNeg, "hasNeg: %s + %s - %s", curBal, price, assets) + } + if order.IsBidOrder() { + rv.Coins, hasNeg = curBal.Add(assets).SafeSub(price) + s.Require().False(hasNeg, "hasNeg: %s + %s - %s", curBal, assets, price) + } + + settleFees := order.GetSettlementFees() + if !settleFees.IsZero() { + orig := rv.Coins + rv.Coins, hasNeg = rv.Coins.SafeSub(settleFees...) + s.Require().False(hasNeg, "hasNeg (settlement fees): %s - %s", orig, settleFees) + } + + for _, fee := range creationFees { + orig := rv.Coins + rv.Coins, hasNeg = rv.Coins.SafeSub(fee) + s.Require().False(hasNeg, "hasNeg (creation fee): %s - %s", orig, fee) + } + + return rv +} + +// assertBalancesFollowup returns a follow-up function that asserts that the balances are now as expected. +func (s *CmdTestSuite) assertBalancesFollowup(expBals []banktypes.Balance) func(*sdk.TxResponse) { + return func(_ *sdk.TxResponse) { + for _, expBal := range expBals { + actBal := s.queryBankBalances(expBal.Address) + s.Assert().Equal(expBal.Coins.String(), actBal.String(), "%s balances", s.getAddrName(expBal.Address)) + } + } +} + +// createOrder issues a command to create the provided order and returns its order id. +func (s *CmdTestSuite) createOrder(order *exchange.Order, creationFee *sdk.Coin) uint64 { + cmd := cli.CmdTx() + args := []string{ + order.GetOrderType(), + "--market", fmt.Sprintf("%d", order.GetMarketID()), + "--from", order.GetOwner(), + "--assets", order.GetAssets().String(), + "--price", order.GetPrice().String(), + } + settleFee := order.GetSettlementFees() + if !settleFee.IsZero() { + args = append(args, "--settlement-fee", settleFee.String()) + } + if order.PartialFillAllowed() { + args = append(args, "--partial") + } + eid := order.GetExternalID() + if len(eid) > 0 { + args = append(args, "--external-id", eid) + } + if creationFee != nil { + args = append(args, "--creation-fee", creationFee.String()) + } + args = append(args, + "--"+flags.FlagFees, s.bondCoins(10).String(), + "--"+flags.FlagBroadcastMode, flags.BroadcastBlock, + "--"+flags.FlagSkipConfirmation, + ) + + cmdName := cmd.Name() + var outBz []byte + defer func() { + if s.T().Failed() { + s.T().Logf("Command: %s\nArgs: %q\nOutput\n%s", cmdName, args, string(outBz)) + } + }() + + clientCtx := s.getClientCtx() + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args) + outBz = out.Bytes() + + s.Require().NoError(err, "ExecTestCLICmd error") + + var resp sdk.TxResponse + err = clientCtx.Codec.UnmarshalJSON(outBz, &resp) + s.Require().NoError(err, "UnmarshalJSON(command output) error") + orderIDStr, err := s.findNewOrderID(&resp) + s.Require().NoError(err, "findNewOrderID") + return s.asOrderID(orderIDStr) +} + +// queryBankBalances executes a bank query to get an account's balances. +func (s *CmdTestSuite) queryBankBalances(addr string) sdk.Coins { + clientCtx := s.getClientCtx() + cmd := bankcli.GetBalancesCmd() + args := []string{addr, "--output", "json"} + outBW, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args) + s.Require().NoError(err, "ExecTestCLICmd %s %q", cmd.Name(), args) + outBz := outBW.Bytes() + + var resp banktypes.QueryAllBalancesResponse + err = clientCtx.Codec.UnmarshalJSON(outBz, &resp) + s.Require().NoError(err, "UnmarshalJSON(%q, %T)", string(outBz), &resp) + return resp.Balances +} + +// execBankSend executes a bank send command. +func (s *CmdTestSuite) execBankSend(fromAddr, toAddr, amount string) { + clientCtx := s.getClientCtx() + cmd := bankcli.NewSendTxCmd() + cmdName := cmd.Name() + args := []string{ + fromAddr, toAddr, amount, + "--" + flags.FlagFees, s.bondCoins(10).String(), + "--" + flags.FlagBroadcastMode, flags.BroadcastBlock, + "--" + flags.FlagSkipConfirmation, + } + failed := true + var outStr string + defer func() { + if failed { + s.T().Logf("Command: %s\nArgs: %q\nOutput\n%s", cmdName, args, outStr) + } + }() + + outBW, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, args) + outStr = outBW.String() + s.Require().NoError(err, "ExecTestCLICmd %s %q", cmdName, args) + failed = false +} + +// joinErrs joins the provided error strings matching to how errors.Join does. +func joinErrs(errs ...string) string { + return strings.Join(errs, "\n") +} + +// toStringSlice applies the stringer to each value and returns a slice with the results. +// +// T is the type of things being converted to strings. +func toStringSlice[T any](vals []T, stringer func(T) string) []string { + if vals == nil { + return nil + } + rv := make([]string, len(vals)) + for i, val := range vals { + rv[i] = stringer(val) + } + return rv +} + +// assertEqualSlices asserts that the two slices are equal; returns true if so. +// If not, the stringer is applied to each entry and the comparison is redone +// using the strings for a more helpful failure message. +// +// T is the type of things being compared (and possibly converted to strings). +func assertEqualSlices[T any](t *testing.T, expected []T, actual []T, stringer func(T) string, message string, args ...interface{}) bool { + t.Helper() + if assert.Equalf(t, expected, actual, message, args...) { + return true + } + expStrs := toStringSlice(expected, stringer) + actStrs := toStringSlice(actual, stringer) + assert.Equalf(t, expStrs, actStrs, message+" as strings", args...) + return false +} + +// splitStringer makes a string from the provided DenomSplit. +func splitStringer(split exchange.DenomSplit) string { + return fmt.Sprintf("%s:%d", split.Denom, split.Split) +} + +// orderIDStringer converts an order id to a string. +func orderIDStringer(orderID uint64) string { + return fmt.Sprintf("%d", orderID) +} + +// asOrderID converts the provided string into an order id. +func (s *CmdTestSuite) asOrderID(str string) uint64 { + rv, err := strconv.ParseUint(str, 10, 64) + s.Require().NoError(err, "ParseUint(%q, 10, 64)", str) + return rv +} + +// truncate truncates the provided string returning at most length characters. +func truncate(str string, length int) string { + if len(str) < length-3 { + return str + } + return str[:length-3] + "..." +} + +const ( + // mutExc is the annotation type for "mutually exclusive". + // It equals the cobra.Command.mutuallyExclusive variable. + mutExc = "cobra_annotation_mutually_exclusive" + // oneReq is the annotation type for "one required". + // It equals the cobra.Command.oneRequired variable. + oneReq = "cobra_annotation_one_required" + // mutExc is the annotation type for "required". + required = cobra.BashCompOneRequiredFlag +) + +// setupTestCase contains the stuff that runSetupTestCase should check. +type setupTestCase struct { + // name is the name of the setup func being tested. + name string + // setup is the function being tested. + setup func(cmd *cobra.Command) + // expFlags is the list of flags expected to be added to the command after setup. + // The flags.FlagFrom flag is added to the command prior to calling the setup func; + // it should be included in this list if you want to check its annotations. + expFlags []string + // expAnnotations is the annotations expected for each of the expFlags. + // The map is "flag name" -> "annotation type" -> values + // The following variables have the annotation type strings: mutExc, oneReq, required. + // Annotations are only checked on the flags listed in expFlags. + expAnnotations map[string]map[string][]string + // expInUse is a set of strings that are expected to be in the command's Use string. + // Each entry that does not start with a "[" is also checked to not be in the Use wrapped in []. + expInUse []string + // expExamples is a set of examples to ensure are on the command. + // There must be a full line in the command's Example that matches each entry. + expExamples []string + // skipArgsCheck true causes the runner to skip the check ensuring that the command's Args func has been set. + skipArgsCheck bool +} + +// runSetupTestCase runs the provided setup func and checks that everything is set up as expected. +func runSetupTestCase(t *testing.T, tc setupTestCase) { + if tc.expAnnotations == nil { + tc.expAnnotations = make(map[string]map[string][]string) + } + cmd := &cobra.Command{ + Use: "dummy", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the dummy command should not have been executed") + }, + } + cmd.Flags().String(flags.FlagFrom, "", "The from flag") + + testFunc := func() { + tc.setup(cmd) + } + require.NotPanics(t, testFunc, tc.name) + + for i, flagName := range tc.expFlags { + t.Run(fmt.Sprintf("flag[%d]: --%s", i, flagName), func(t *testing.T) { + flag := cmd.Flags().Lookup(flagName) + if assert.NotNil(t, flag, "--%s", flagName) { + expAnnotations, _ := tc.expAnnotations[flagName] + actAnnotations := flag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "--%s annotations", flagName) + } + }) + } + + for i, exp := range tc.expInUse { + t.Run(fmt.Sprintf("use[%d]: %s", i, truncate(exp, 20)), func(t *testing.T) { + assert.Contains(t, cmd.Use, exp, "command use after %s", tc.name) + if exp[0] != '[' { + assert.NotContains(t, cmd.Use, "["+exp+"]", "command use after %s", tc.name) + } + }) + } + + examples := strings.Split(cmd.Example, "\n") + for i, exp := range tc.expExamples { + t.Run(fmt.Sprintf("examples[%d]", i), func(t *testing.T) { + assert.Contains(t, examples, exp, "command examples after %s", tc.name) + }) + } + + if !tc.skipArgsCheck { + t.Run("args", func(t *testing.T) { + assert.NotNil(t, cmd.Args, "command args after %s", tc.name) + }) + } +} + +// newClientContextWithCodec returns a new client.Context that has a useful Codec. +func newClientContextWithCodec() client.Context { + return clientContextWithCodec(client.Context{}) +} + +// clientContextWithCodec adds a useful Codec to the provided client context. +func clientContextWithCodec(clientCtx client.Context) client.Context { + encCfg := app.MakeEncodingConfig() + return clientCtx. + WithCodec(encCfg.Marshaler). + WithInterfaceRegistry(encCfg.InterfaceRegistry). + WithTxConfig(encCfg.TxConfig) +} + +// newGovProp creates a new MsgSubmitProposal containing the provided messages, requiring it to not error. +func newGovProp(t *testing.T, msgs ...sdk.Msg) *govv1.MsgSubmitProposal { + rv := &govv1.MsgSubmitProposal{} + for _, msg := range msgs { + msgAny, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err, "NewAnyWithValue(%T)", msg) + rv.Messages = append(rv.Messages, msgAny) + } + return rv +} + +// newTx creates a new Tx containing the provided messages, requiring it to not error. +func newTx(t *testing.T, msgs ...sdk.Msg) *txtypes.Tx { + rv := &txtypes.Tx{ + Body: &txtypes.TxBody{}, + AuthInfo: &txtypes.AuthInfo{}, + Signatures: make([][]byte, 0), + } + for _, msg := range msgs { + msgAny, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err, "NewAnyWithValue(%T)", msg) + rv.Body.Messages = append(rv.Body.Messages, msgAny) + } + return rv +} + +// writeFileAsJson writes the provided proto message as a json file, requiring it to not error. +func writeFileAsJson(t *testing.T, filename string, content proto.Message) { + clientCtx := newClientContextWithCodec() + bz, err := clientCtx.Codec.MarshalJSON(content) + require.NoError(t, err, "MarshalJSON(%T)", content) + writeFile(t, filename, bz) +} + +// writeFile writes a file requiring it to not error. +func writeFile(t *testing.T, filename string, bz []byte) { + err := os.WriteFile(filename, bz, 0o644) + require.NoError(t, err, "WriteFile(%q)", filename) +} + +// getAnyTypes gets the TypeURL field of each of the provided anys. +func getAnyTypes(anys []*codectypes.Any) []string { + rv := make([]string, len(anys)) + for i, a := range anys { + rv[i] = a.GetTypeUrl() + } + return rv +} diff --git a/x/exchange/client/cli/flags.go b/x/exchange/client/cli/flags.go new file mode 100644 index 0000000000..af3f3f55b8 --- /dev/null +++ b/x/exchange/client/cli/flags.go @@ -0,0 +1,693 @@ +package cli + +import ( + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/provenance-io/provenance/x/exchange" +) + +const ( + FlagAcceptingOrders = "accepting-orders" + FlagAccessGrants = "access-grants" + FlagAdmin = "admin" + FlagAfter = "after" + FlagAllowUserSettle = "allow-user-settle" + FlagAmount = "amount" + FlagAsk = "ask" + FlagAskAdd = "ask-add" + FlagAskRemove = "ask-remove" + FlagAsks = "asks" + FlagAssets = "assets" + FlagAuthority = "authority" + FlagBid = "bid" + FlagBidAdd = "bid-add" + FlagBidRemove = "bid-remove" + FlagBids = "bids" + FlagBuyer = "buyer" + FlagBuyerFlat = "buyer-flat" + FlagBuyerFlatAdd = "buyer-flat-add" + FlagBuyerFlatRemove = "buyer-flat-remove" + FlagBuyerRatios = "buyer-ratios" + FlagBuyerRatiosAdd = "buyer-ratios-add" + FlagBuyerRatiosRemove = "buyer-ratios-remove" + FlagCreateAsk = "create-ask" + FlagCreateBid = "create-bid" + FlagCreationFee = "creation-fee" + FlagDefault = "default" + FlagDenom = "denom" + FlagDescription = "description" + FlagDisable = "disable" + FlagEnable = "enable" + FlagExternalID = "external-id" + FlagGrant = "grant" + FlagIcon = "icon" + FlagMarket = "market" + FlagName = "name" + FlagOrder = "order" + FlagOwner = "owner" + FlagPartial = "partial" + FlagPrice = "price" + FlagProposal = "proposal" + FlagReqAttrAsk = "req-attr-ask" + FlagReqAttrBid = "req-attr-bid" + FlagRevoke = "revoke" + FlagRevokeAll = "revoke-all" + FlagSeller = "seller" + FlagSellerFlat = "seller-flat" + FlagSellerFlatAdd = "seller-flat-add" + FlagSellerFlatRemove = "seller-flat-remove" + FlagSellerRatios = "seller-ratios" + FlagSellerRatiosAdd = "seller-ratios-add" + FlagSellerRatiosRemove = "seller-ratios-remove" + FlagSettlementFee = "settlement-fee" + FlagSigner = "signer" + FlagSplit = "split" + FlagTo = "to" + FlagURL = "url" +) + +// MarkFlagsRequired marks the provided flags as required and panics if there's a problem. +func MarkFlagsRequired(cmd *cobra.Command, names ...string) { + for _, name := range names { + if err := cmd.MarkFlagRequired(name); err != nil { + panic(fmt.Errorf("error marking --%s flag required on %s: %w", name, cmd.Name(), err)) + } + } +} + +// AddFlagsAdmin adds the --admin and --authority flags to a command and makes them mutually exclusive. +// It also makes one of --admin, --authority, and --from required. +// +// Use ReadFlagsAdminOrFrom to read these flags. +func AddFlagsAdmin(cmd *cobra.Command) { + cmd.Flags().String(FlagAdmin, "", "The admin (defaults to --from account)") + cmd.Flags().Bool(FlagAuthority, false, "Use the governance module account for the admin") + + cmd.MarkFlagsMutuallyExclusive(FlagAdmin, FlagAuthority) + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagAdmin, FlagAuthority) +} + +// ReadFlagsAdminOrFrom reads the --admin flag if provided. +// If not, but the --authority flag was provided, the gov module account address is returned. +// If no -admin or --authority flag was provided, returns the --from address. +// Returns an error if none of those flags were provided or there was an error reading one. +// +// This assumes AddFlagsAdmin was used to define the flags, and that the context comes from client.GetClientTxContext. +func ReadFlagsAdminOrFrom(clientCtx client.Context, flagSet *pflag.FlagSet) (string, error) { + rv, err := flagSet.GetString(FlagAdmin) + if len(rv) > 0 || err != nil { + return rv, err + } + + useAuth, err := flagSet.GetBool(FlagAuthority) + if err != nil { + return "", err + } + if useAuth { + return AuthorityAddr.String(), nil + } + + rv = clientCtx.GetFromAddress().String() + if len(rv) > 0 { + return rv, nil + } + + return "", errors.New("no provided") +} + +// ReadFlagAuthority reads the --authority flag, or if not provided, returns the standard authority address. +// This assumes that the flag was defined with a default of "". +func ReadFlagAuthority(flagSet *pflag.FlagSet) (string, error) { + return ReadFlagAuthorityOrDefault(flagSet, AuthorityAddr.String()) +} + +// ReadFlagAuthorityOrDefault reads the --authority flag, or if not provided, returns the default. +// If the provided default is "", the standard authority address is used as the default. +// This assumes that the flag was defined with a default of "". +func ReadFlagAuthorityOrDefault(flagSet *pflag.FlagSet, def string) (string, error) { + rv, err := flagSet.GetString(FlagAuthority) + if len(rv) == 0 || err != nil { + if len(def) > 0 { + return def, err + } + return AuthorityAddr.String(), err + } + return rv, nil +} + +// ReadAddrFlagOrFrom gets the requested flag or, if it wasn't provided, gets the --from address. +// Returns an error if neither the flag nor --from were provided. +// This assumes that the flag was defined with a default of "". +func ReadAddrFlagOrFrom(clientCtx client.Context, flagSet *pflag.FlagSet, name string) (string, error) { + rv, err := flagSet.GetString(name) + if len(rv) > 0 || err != nil { + return rv, err + } + + rv = clientCtx.GetFromAddress().String() + if len(rv) > 0 { + return rv, nil + } + + return "", fmt.Errorf("no <%s> provided", name) +} + +// AddFlagsEnableDisable adds the --enable and --disable flags and marks them mutually exclusive and one is required. +// +// Use ReadFlagsEnableDisable to read these flags. +func AddFlagsEnableDisable(cmd *cobra.Command, name string) { + cmd.Flags().Bool(FlagEnable, false, fmt.Sprintf("Set the market's %s field to true", name)) + cmd.Flags().Bool(FlagDisable, false, fmt.Sprintf("Set the market's %s field to false", name)) + cmd.MarkFlagsMutuallyExclusive(FlagEnable, FlagDisable) + cmd.MarkFlagsOneRequired(FlagEnable, FlagDisable) +} + +// ReadFlagsEnableDisable reads the --enable and --disable flags. +// If --enable is given, returns true, if --disable is given, returns false. +// +// This assumes that the flags were defined with AddFlagsEnableDisable. +func ReadFlagsEnableDisable(flagSet *pflag.FlagSet) (bool, error) { + enable, err := flagSet.GetBool(FlagEnable) + if enable || err != nil { + return enable, err + } + disable, err := flagSet.GetBool(FlagDisable) + if disable || err != nil { + return false, err + } + return false, fmt.Errorf("exactly one of --%s or --%s must be provided", FlagEnable, FlagDisable) +} + +// AddFlagsAsksBidsBools adds the --asks and --bids flags as bools for limiting search results. +// Marks them mutually exclusive (but not required). +// +// Use ReadFlagsAsksBidsOpt to read them. +func AddFlagsAsksBidsBools(cmd *cobra.Command) { + cmd.Flags().Bool(FlagAsks, false, "Limit results to only ask orders") + cmd.Flags().Bool(FlagBids, false, "Limit results to only bid orders") + cmd.MarkFlagsMutuallyExclusive(FlagAsks, FlagBids) +} + +// ReadFlagsAsksBidsOpt reads the --asks and --bids bool flags, returning either "ask", "bid" or "". +// +// This assumes that the flags were defined using AddFlagsAsksBidsBools. +func ReadFlagsAsksBidsOpt(flagSet *pflag.FlagSet) (string, error) { + isAsk, err := flagSet.GetBool(FlagAsks) + if err != nil { + return "", err + } + if isAsk { + return "ask", nil + } + + isBid, err := flagSet.GetBool(FlagBids) + if err != nil { + return "", err + } + if isBid { + return "bid", nil + } + + return "", nil +} + +// ReadFlagOrderOrArg gets a required order id from either the --order flag or the first provided arg. +// This assumes that the flag was defined with a default of 0. +func ReadFlagOrderOrArg(flagSet *pflag.FlagSet, args []string) (uint64, error) { + orderID, err := flagSet.GetUint64(FlagOrder) + if err != nil { + return 0, err + } + + if len(args) > 0 && len(args[0]) > 0 { + if orderID != 0 { + return 0, fmt.Errorf("cannot provide as both an arg (%q) and flag (--%s %d)", args[0], FlagOrder, orderID) + } + + orderID, err = strconv.ParseUint(args[0], 10, 64) + if err != nil { + return 0, fmt.Errorf("could not convert arg: %w", err) + } + } + + if orderID == 0 { + return 0, errors.New("no provided") + } + + return orderID, nil +} + +// ReadFlagMarketOrArg gets a required market id from either the --market flag or the first provided arg. +// This assumes that the flag was defined with a default of 0. +func ReadFlagMarketOrArg(flagSet *pflag.FlagSet, args []string) (uint32, error) { + marketID, err := flagSet.GetUint32(FlagMarket) + if err != nil { + return 0, err + } + + if len(args) > 0 && len(args[0]) > 0 { + if marketID != 0 { + return 0, fmt.Errorf("cannot provide as both an arg (%q) and flag (--%s %d)", args[0], FlagMarket, marketID) + } + + var marketID64 uint64 + marketID64, err = strconv.ParseUint(args[0], 10, 32) + if err != nil { + return 0, fmt.Errorf("could not convert arg: %w", err) + } + marketID = uint32(marketID64) + } + + if marketID == 0 { + return 0, errors.New("no provided") + } + + return marketID, nil +} + +// ReadCoinsFlag reads a string flag and converts it into sdk.Coins. +// If the flag wasn't provided, this returns nil, nil. +// +// If the flag is a StringSlice, use ReadFlatFeeFlag. +func ReadCoinsFlag(flagSet *pflag.FlagSet, name string) (sdk.Coins, error) { + value, err := flagSet.GetString(name) + if len(value) == 0 || err != nil { + return nil, err + } + rv, err := ParseCoins(value) + if err != nil { + return nil, fmt.Errorf("error parsing --%s as coins: %w", name, err) + } + return rv, nil +} + +// ParseCoins parses a string into sdk.Coins. +func ParseCoins(coinsStr string) (sdk.Coins, error) { + // The sdk.ParseCoinsNormalized func allows for decimals and just truncates if there are some. + // But I want an error if there's a decimal portion. + // Its errors also always have "invalid decimal coin expression", and I don't want "decimal" in these errors. + // I also like having the offending coin string quoted since its safer and clarifies when the coinsStr is "". + if len(coinsStr) == 0 { + return nil, nil + } + var rv sdk.Coins + for _, coinStr := range strings.Split(coinsStr, ",") { + c, err := exchange.ParseCoin(coinStr) + if err != nil { + return nil, err + } + rv = rv.Add(c) + } + return rv, nil +} + +// ReadCoinFlag reads a string flag and converts it into *sdk.Coin. +// If the flag wasn't provided, this returns nil, nil. +// +// Use ReadReqCoinFlag if the flag is required. +func ReadCoinFlag(flagSet *pflag.FlagSet, name string) (*sdk.Coin, error) { + value, err := flagSet.GetString(name) + if len(value) == 0 || err != nil { + return nil, err + } + rv, err := exchange.ParseCoin(value) + if err != nil { + return nil, fmt.Errorf("error parsing --%s as a coin: %w", name, err) + } + return &rv, nil +} + +// ReadReqCoinFlag reads a string flag and converts it into a sdk.Coin and requires it to have a value. +// Returns an error if not provided. +// +// Use ReadCoinFlag if the flag is optional. +func ReadReqCoinFlag(flagSet *pflag.FlagSet, name string) (sdk.Coin, error) { + rv, err := ReadCoinFlag(flagSet, name) + if err != nil { + return sdk.Coin{}, err + } + if rv == nil { + return sdk.Coin{}, fmt.Errorf("missing required --%s flag", name) + } + return *rv, nil +} + +// ReadOrderIDsFlag reads a UintSlice flag and converts it into a []uint64. +func ReadOrderIDsFlag(flagSet *pflag.FlagSet, name string) ([]uint64, error) { + ids, err := flagSet.GetUintSlice(name) + if len(ids) == 0 || err != nil { + return nil, err + } + rv := make([]uint64, len(ids)) + for i, id := range ids { + rv[i] = uint64(id) + } + return rv, nil +} + +// ReadAccessGrantsFlag reads a StringSlice flag and converts it to a slice of AccessGrants. +// This assumes that the flag was defined with a default of nil or []string{}. +func ReadAccessGrantsFlag(flagSet *pflag.FlagSet, name string, def []exchange.AccessGrant) ([]exchange.AccessGrant, error) { + vals, err := flagSet.GetStringSlice(name) + if len(vals) == 0 || err != nil { + return def, err + } + return ParseAccessGrants(vals) +} + +// permSepRx is a regexp that matches characters that can be used to separate permissions. +var permSepRx = regexp.MustCompile(`[ +.]`) + +// ParseAccessGrant parses an AccessGrant from a string with the format "
:[+...]". +func ParseAccessGrant(val string) (*exchange.AccessGrant, error) { + parts := strings.Split(val, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("could not parse %q as an : expected format
:", val) + } + + addr := strings.TrimSpace(parts[0]) + perms := strings.ToLower(strings.TrimSpace(parts[1])) + if len(addr) == 0 || len(perms) == 0 { + return nil, fmt.Errorf("invalid %q: both an
and are required", val) + } + + rv := &exchange.AccessGrant{Address: addr} + + if perms == "all" { + rv.Permissions = exchange.AllPermissions() + return rv, nil + } + + permVals := permSepRx.Split(perms, -1) + var err error + rv.Permissions, err = exchange.ParsePermissions(permVals...) + if err != nil { + return nil, fmt.Errorf("could not parse permissions for %q from %q: %w", rv.Address, parts[1], err) + } + + return rv, nil +} + +// ParseAccessGrants parses an AccessGrant from each of the provided vals. +func ParseAccessGrants(vals []string) ([]exchange.AccessGrant, error) { + var errs []error + grants := make([]exchange.AccessGrant, 0, len(vals)) + for _, val := range vals { + ag, err := ParseAccessGrant(val) + if err != nil { + errs = append(errs, err) + } + if ag != nil { + grants = append(grants, *ag) + } + } + return grants, errors.Join(errs...) +} + +// ReadFlatFeeFlag reads a StringSlice flag and converts it into a slice of sdk.Coin. +// If the flag wasn't provided, the provided default is returned. +// This assumes that the flag was defined with a default of nil or []string{}. +// +// If the flag is a String, use ReadCoinsFlag. +func ReadFlatFeeFlag(flagSet *pflag.FlagSet, name string, def []sdk.Coin) ([]sdk.Coin, error) { + vals, err := flagSet.GetStringSlice(name) + if len(vals) == 0 || err != nil { + return def, err + } + return ParseFlatFeeOptions(vals) +} + +// ParseFlatFeeOptions parses each of the provided vals to sdk.Coin. +func ParseFlatFeeOptions(vals []string) ([]sdk.Coin, error) { + var errs []error + rv := make([]sdk.Coin, 0, len(vals)) + for _, val := range vals { + coin, err := exchange.ParseCoin(val) + if err != nil { + errs = append(errs, err) + } else { + rv = append(rv, coin) + } + } + return rv, errors.Join(errs...) +} + +// ReadFeeRatiosFlag reads a StringSlice flag and converts it into a slice of exchange.FeeRatio. +// If the flag wasn't provided, the provided default is returned. +// This assumes that the flag was defined with a default of nil or []string{}. +func ReadFeeRatiosFlag(flagSet *pflag.FlagSet, name string, def []exchange.FeeRatio) ([]exchange.FeeRatio, error) { + vals, err := flagSet.GetStringSlice(name) + if len(vals) == 0 || err != nil { + return def, err + } + return ParseFeeRatios(vals) +} + +// ParseFeeRatios parses a FeeRatio from each of the provided vals. +func ParseFeeRatios(vals []string) ([]exchange.FeeRatio, error) { + var errs []error + ratios := make([]exchange.FeeRatio, 0, len(vals)) + for _, val := range vals { + ratio, err := exchange.ParseFeeRatio(val) + if err != nil { + errs = append(errs, err) + } + if ratio != nil { + ratios = append(ratios, *ratio) + } + } + return ratios, errors.Join(errs...) +} + +// ReadSplitsFlag reads a StringSlice flag and converts it into a slice of exchange.DenomSplit. +// This assumes that the flag was defined with a default of nil or []string{}. +func ReadSplitsFlag(flagSet *pflag.FlagSet, name string) ([]exchange.DenomSplit, error) { + vals, err := flagSet.GetStringSlice(name) + if len(vals) == 0 || err != nil { + return nil, err + } + return ParseSplits(vals) +} + +// ParseSplit parses a DenomSplit from a string with the format ":". +func ParseSplit(val string) (*exchange.DenomSplit, error) { + parts := strings.Split(val, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid denom split %q: expected format :", val) + } + + denom := strings.TrimSpace(parts[0]) + amountStr := strings.TrimSpace(parts[1]) + if len(denom) == 0 || len(amountStr) == 0 { + return nil, fmt.Errorf("invalid denom split %q: both a and are required", val) + } + + amount, err := strconv.ParseUint(amountStr, 10, 32) + if err != nil { + return nil, fmt.Errorf("could not parse %q amount: %w", val, err) + } + + return &exchange.DenomSplit{Denom: denom, Split: uint32(amount)}, nil +} + +// ParseSplits parses a DenomSplit from each of the provided vals. +func ParseSplits(vals []string) ([]exchange.DenomSplit, error) { + var errs []error + splits := make([]exchange.DenomSplit, 0, len(vals)) + for _, val := range vals { + split, err := ParseSplit(val) + if err != nil { + errs = append(errs, err) + } + if split != nil { + splits = append(splits, *split) + } + } + return splits, errors.Join(errs...) +} + +// ReadStringFlagOrArg gets a required string from either a flag or the first provided arg. +// This assumes that the flag was defined with a default of "". +func ReadStringFlagOrArg(flagSet *pflag.FlagSet, args []string, flagName, varName string) (string, error) { + rv, err := flagSet.GetString(flagName) + if err != nil { + return "", err + } + + if len(args) > 0 && len(args[0]) > 0 { + if len(rv) > 0 { + return "", fmt.Errorf("cannot provide <%s> as both an arg (%q) and flag (--%s %q)", varName, args[0], flagName, rv) + } + + return args[0], nil + } + + if len(rv) == 0 { + return "", fmt.Errorf("no <%s> provided", varName) + } + + return rv, nil +} + +// ReadProposalFlag gets the --proposal string value and attempts to read the file in as a Tx in json. +// It then attempts to extract any messages contained in any govv1.MsgSubmitProposal messages in that Tx. +// An error is returned if anything goes wrong. +// This assumes that the flag was defined with a default of "". +func ReadProposalFlag(clientCtx client.Context, flagSet *pflag.FlagSet) (string, []*codectypes.Any, error) { + propFN, err := flagSet.GetString(FlagProposal) + if len(propFN) == 0 || err != nil { + return "", nil, err + } + + propFileContents, err := os.ReadFile(propFN) + if err != nil { + return propFN, nil, err + } + + var tx txtypes.Tx + err = clientCtx.Codec.UnmarshalJSON(propFileContents, &tx) + if err != nil { + return propFN, nil, fmt.Errorf("failed to unmarshal --%s %q contents as Tx: %w", FlagProposal, propFN, err) + } + + if tx.Body == nil { + return propFN, nil, fmt.Errorf("the contents of %q does not have a \"body\"", propFN) + } + + if len(tx.Body.Messages) == 0 { + return propFN, nil, fmt.Errorf("the contents of %q does not have any body messages", propFN) + } + + hadProp := false + var rv []*codectypes.Any + for _, msgAny := range tx.Body.Messages { + prop, isProp := msgAny.GetCachedValue().(*govv1.MsgSubmitProposal) + if isProp { + hadProp = true + rv = append(rv, prop.Messages...) + } + } + + if !hadProp { + return propFN, nil, fmt.Errorf("no %T messages found in %q", &govv1.MsgSubmitProposal{}, propFN) + } + if len(rv) == 0 { + return propFN, nil, fmt.Errorf("no messages found in any %T messages in %q", &govv1.MsgSubmitProposal{}, propFN) + } + + return propFN, rv, nil +} + +// getSingleMsgFromPropFlag reads the --proposal flag and extracts a Msg of a specific type from the file it points to. +// If --proposal wasn't provided, the emptyMsg is returned without error. +// An error is returned if anything goes wrong or the file doesn't have exactly one T. +// The emptyMsg is returned even if an error is returned. +// +// T is the specific type of Msg to look for. +func getSingleMsgFromPropFlag[T sdk.Msg](clientCtx client.Context, flagSet *pflag.FlagSet, emptyMsg T) (T, error) { + fn, msgs, err := ReadProposalFlag(clientCtx, flagSet) + if len(fn) == 0 || err != nil { + return emptyMsg, err + } + + rvs := make([]T, 0, 1) + for _, msg := range msgs { + rv, isRV := msg.GetCachedValue().(T) + if isRV { + rvs = append(rvs, rv) + } + } + + if len(rvs) == 0 { + return emptyMsg, fmt.Errorf("no %T found in %q", emptyMsg, fn) + } + if len(rvs) != 1 { + return emptyMsg, fmt.Errorf("%d %T found in %q", len(rvs), emptyMsg, fn) + } + + return rvs[0], nil +} + +// ReadMsgGovCreateMarketRequestFromProposalFlag reads the --proposal flag and extracts the MsgGovCreateMarketRequest from the file points to. +// An error is returned if anything goes wrong or the file doesn't have exactly one MsgGovCreateMarketRequest. +// A MsgGovCreateMarketRequest is returned even if an error is returned. +// This assumes that the flag was defined with a default of "". +func ReadMsgGovCreateMarketRequestFromProposalFlag(clientCtx client.Context, flagSet *pflag.FlagSet) (*exchange.MsgGovCreateMarketRequest, error) { + return getSingleMsgFromPropFlag(clientCtx, flagSet, &exchange.MsgGovCreateMarketRequest{}) +} + +// ReadMsgGovManageFeesRequestFromProposalFlag reads the --proposal flag and extracts the MsgGovManageFeesRequest from the file points to. +// An error is returned if anything goes wrong or the file doesn't have exactly one MsgGovManageFeesRequest. +// A MsgGovManageFeesRequest is returned even if an error is returned. +// This assumes that the flag was defined with a default of "". +func ReadMsgGovManageFeesRequestFromProposalFlag(clientCtx client.Context, flagSet *pflag.FlagSet) (*exchange.MsgGovManageFeesRequest, error) { + return getSingleMsgFromPropFlag(clientCtx, flagSet, &exchange.MsgGovManageFeesRequest{}) +} + +// ReadFlagUint32OrDefault gets a uit32 flag or returns the provided default. +// This assumes that the flag was defined with a default of 0. +func ReadFlagUint32OrDefault(flagSet *pflag.FlagSet, name string, def uint32) (uint32, error) { + rv, err := flagSet.GetUint32(name) + if rv == 0 || err != nil { + return def, err + } + return rv, nil +} + +// ReadFlagBoolOrDefault gets a bool flag or returns the provided default. +// This assumes that the flag was defined with a default of false (it actually just ignores that default). +func ReadFlagBoolOrDefault(flagSet *pflag.FlagSet, name string, def bool) (bool, error) { + // A bool flag is a little different from the others. + // If someone provides --=false, I want to use that instead of the provided default. + // The default in here should only be used if there's an error or the flag wasn't given. + // This effectively ignores if the flag was defined with a default of true, which shouldn't be done anyway. + rv, err := flagSet.GetBool(name) + if err != nil { + return def, err + } + flagGiven := false + flagSet.Visit(func(flag *pflag.Flag) { + if flag.Name == name { + flagGiven = true + } + }) + if flagGiven { + return rv, nil + } + return def, nil +} + +// ReadFlagStringSliceOrDefault gets a string slice flag or returns the provided default. +// This assumes that the flag was defined with a default of nil or []string{}. +func ReadFlagStringSliceOrDefault(flagSet *pflag.FlagSet, name string, def []string) ([]string, error) { + rv, err := flagSet.GetStringSlice(name) + if len(rv) == 0 || err != nil { + return def, err + } + return rv, nil +} + +// ReadFlagStringOrDefault gets a string flag or returns the provided default. +// This assumes that the flag was defined with a default of "". +func ReadFlagStringOrDefault(flagSet *pflag.FlagSet, name string, def string) (string, error) { + rv, err := flagSet.GetString(name) + if len(rv) == 0 || err != nil { + return def, err + } + return rv, nil +} diff --git a/x/exchange/client/cli/flags_test.go b/x/exchange/client/cli/flags_test.go new file mode 100644 index 0000000000..5ab0d9e9ff --- /dev/null +++ b/x/exchange/client/cli/flags_test.go @@ -0,0 +1,2719 @@ +package cli_test + +import ( + "errors" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" +) + +const ( + flagBool = "bool" + flagInt = "int" + flagString = "string" + flagStringSlice = "string-slice" + flagUintSlice = "uint-slice" + flagUint32 = "uint32" +) + +func TestMarkFlagsRequired(t *testing.T) { + flagOne := "one" + flagTwo := "two" + flagThree := "three" + expAnnotations := map[string][]string{ + cobra.BashCompOneRequiredFlag: {"true"}, + } + + tests := []struct { + name string + names []string + expPanic string + }{ + { + name: "no names", + names: []string{}, + expPanic: "", + }, + { + name: "one name, exists", + names: []string{flagOne}, + expPanic: "", + }, + { + name: "one name, not found", + names: []string{"nope"}, + expPanic: "error marking --nope flag required on testing: no such flag -nope", + }, + { + name: "three names, first not found", + names: []string{"gold", flagThree, flagThree}, + expPanic: "error marking --gold flag required on testing: no such flag -gold", + }, + { + name: "three names, second not found", + names: []string{flagOne, "missing", flagThree}, + expPanic: "error marking --missing flag required on testing: no such flag -missing", + }, + { + name: "three names, third not found", + names: []string{flagOne, flagThree, "derp"}, + expPanic: "error marking --derp flag required on testing: no such flag -derp", + }, + { + name: "three names, all exist", + names: []string{flagOne, flagThree, flagThree}, + expPanic: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "testing", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + cmd.Flags().String(flagOne, "", "The one") + cmd.Flags().Bool(flagTwo, false, "The next best") + cmd.Flags().Int(flagThree, 0, "Bronze") + + testFunc := func() { + cli.MarkFlagsRequired(cmd, tc.names...) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "MarkFlagsRequired(%q)", tc.names) + if len(tc.expPanic) > 0 { + return + } + + cmdFlags := cmd.Flags() + + for _, name := range tc.names { + flag := cmdFlags.Lookup(name) + if assert.NotNil(t, flag, "The --%s flag", name) { + actAnnotations := flag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "The --%s flag annotations", name) + } + } + }) + } +} + +func TestAddFlagsAdmin(t *testing.T) { + expAnnotations := map[string][]string{ + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + } + + cmd := &cobra.Command{ + Use: "testing", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + cmd.Flags().String(flags.FlagFrom, "", "The from flag") + cli.AddFlagsAdmin(cmd) + + adminFlag := cmd.Flags().Lookup(cli.FlagAdmin) + if assert.NotNil(t, adminFlag, "The --%s flag", cli.FlagAdmin) { + expUsage := "The admin (defaults to --from account)" + actUsage := adminFlag.Usage + assert.Equal(t, expUsage, actUsage, "The --%s flag usage", cli.FlagAdmin) + actAnnotations := adminFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "The --%s flag annotations", cli.FlagAdmin) + } + + authorityFlag := cmd.Flags().Lookup(cli.FlagAuthority) + if assert.NotNil(t, authorityFlag, "The --%s flag", cli.FlagAuthority) { + expUsage := "Use the governance module account for the admin" + actUsage := authorityFlag.Usage + assert.Equal(t, expUsage, actUsage, "The --%s flag usage", cli.FlagAuthority) + actAnnotations := authorityFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "The --%s flag annotations", cli.FlagAuthority) + } + + flagFrom := cmd.Flags().Lookup(flags.FlagFrom) + if assert.NotNil(t, flagFrom, "The --%s flag", flags.FlagFrom) { + fromExpAnnotations := map[string][]string{oneReq: expAnnotations[oneReq]} + actAnnotations := flagFrom.Annotations + assert.Equal(t, fromExpAnnotations, actAnnotations, "The --%s flag annotations", flags.FlagFrom) + } +} + +func TestReadFlagsAdminOrFrom(t *testing.T) { + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagAdmin, "", "The admin") + flagSet.Bool(cli.FlagAuthority, false, "Use authority") + return flagSet + } + + tests := []struct { + name string + flagSet func() *pflag.FlagSet + flags []string + clientCtx client.Context + expAddr string + expErr string + }{ + { + name: "wrong admin flag type", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Int(cli.FlagAdmin, 0, "The admin") + flagSet.Bool(cli.FlagAuthority, false, "Use authority") + return flagSet + }, + expErr: "trying to get string value of flag of type int", + }, + { + name: "wrong authority flag type", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagAdmin, "", "The admin") + flagSet.Int(cli.FlagAuthority, 0, "Use authority") + return flagSet + + }, + expErr: "trying to get bool value of flag of type int", + }, + { + name: "admin flag given", + flags: []string{"--" + cli.FlagAdmin, "theadmin"}, + expAddr: "theadmin", + }, + { + name: "authority flag given", + flags: []string{"--" + cli.FlagAuthority}, + expAddr: cli.AuthorityAddr.String(), + }, + { + name: "from address given", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + expAddr: sdk.AccAddress("FromAddress_________").String(), + }, + { + name: "nothing given", + expErr: "no provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.flagSet == nil { + tc.flagSet = goodFlagSet + } + flagSet := tc.flagSet() + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var addr string + testFunc := func() { + addr, err = cli.ReadFlagsAdminOrFrom(tc.clientCtx, flagSet) + } + require.NotPanics(t, testFunc, "ReadFlagsAdminOrFrom") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagsAdminOrFrom error") + assert.Equal(t, tc.expAddr, addr, "ReadFlagsAdminOrFrom address") + }) + } +} + +func TestReadFlagAuthority(t *testing.T) { + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagAuthority, "", "The authority") + return flagSet + } + + tests := []struct { + name string + flagSet func() *pflag.FlagSet + flags []string + expAddr string + expErr string + }{ + { + name: "wrong flag type", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Int(cli.FlagAuthority, 0, "The authority") + return flagSet + + }, + expAddr: cli.AuthorityAddr.String(), + expErr: "trying to get string value of flag of type int", + }, + { + name: "provided", + flagSet: goodFlagSet, + flags: []string{"--" + cli.FlagAuthority, "usemeinstead"}, + expAddr: "usemeinstead", + }, + { + name: "not provided", + flagSet: goodFlagSet, + expAddr: cli.AuthorityAddr.String(), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + flagSet := tc.flagSet() + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var addr string + testFunc := func() { + addr, err = cli.ReadFlagAuthority(flagSet) + } + require.NotPanics(t, testFunc, "ReadFlagAuthority") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagAuthority error") + assert.Equal(t, tc.expAddr, addr, "ReadFlagAuthority address") + }) + } +} + +func TestReadFlagAuthorityOrDefault(t *testing.T) { + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagAuthority, "", "The authority") + return flagSet + } + badFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Int(cli.FlagAuthority, 0, "The authority") + return flagSet + } + + tests := []struct { + name string + flagSet func() *pflag.FlagSet + flags []string + def string + expAddr string + expErr string + }{ + { + name: "wrong flag type, no default", + flagSet: badFlagSet, + expAddr: cli.AuthorityAddr.String(), + expErr: "trying to get string value of flag of type int", + }, + { + name: "wrong flag type, with default", + flagSet: badFlagSet, + def: "thedefault", + expAddr: "thedefault", + expErr: "trying to get string value of flag of type int", + }, + { + name: "provided, no default", + flags: []string{"--" + cli.FlagAuthority, "usemeinstead"}, + expAddr: "usemeinstead", + }, + { + name: "provided, with default", + flags: []string{"--" + cli.FlagAuthority, "usemeinstead"}, + def: "thedefault", + expAddr: "usemeinstead", + }, + { + name: "not provided, no default", + expAddr: cli.AuthorityAddr.String(), + }, + { + name: "not provided, with default", + def: "thedefault", + expAddr: "thedefault", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.flagSet == nil { + tc.flagSet = goodFlagSet + } + flagSet := tc.flagSet() + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var addr string + testFunc := func() { + addr, err = cli.ReadFlagAuthorityOrDefault(flagSet, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagAuthorityOrDefault") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagAuthorityOrDefault error") + assert.Equal(t, tc.expAddr, addr, "ReadFlagAuthorityOrDefault address") + }) + } +} + +func TestReadAddrFlagOrFrom(t *testing.T) { + tests := []struct { + testName string + flags []string + clientCtx client.Context + name string + expAddr string + expErr string + }{ + { + testName: "unknown flag", + name: "notsetup", + expErr: "flag accessed but not defined: notsetup", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get string value of flag of type int", + }, + { + testName: "flag given", + flags: []string{"--" + flagString, "someaddr"}, + name: flagString, + expAddr: "someaddr", + }, + { + testName: "using from", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + name: flagString, + expAddr: sdk.AccAddress("FromAddress_________").String(), + }, + { + testName: "not provided", + name: flagString, + expErr: "no provided", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var addr string + testFunc := func() { + addr, err = cli.ReadAddrFlagOrFrom(tc.clientCtx, flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadAddrFlagOrFrom") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadAddrFlagOrFrom error") + assert.Equal(t, tc.expAddr, addr, "ReadAddrFlagOrFrom address") + }) + } +} + +func TestAddFlagsEnableDisable(t *testing.T) { + expAnnotations := map[string][]string{ + mutExc: {cli.FlagEnable + " " + cli.FlagDisable}, + oneReq: {cli.FlagEnable + " " + cli.FlagDisable}, + } + + cmd := &cobra.Command{ + Use: "testing", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + cli.AddFlagsEnableDisable(cmd, "unittest") + + enableFlag := cmd.Flags().Lookup(cli.FlagEnable) + if assert.NotNil(t, enableFlag, "The --%s flag", cli.FlagEnable) { + expUsage := "Set the market's unittest field to true" + actusage := enableFlag.Usage + assert.Equal(t, expUsage, actusage, "--%s flag usage", cli.FlagEnable) + actAnnotations := enableFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "--%s flag annotations", cli.FlagEnable) + } + + disableFlag := cmd.Flags().Lookup(cli.FlagDisable) + if assert.NotNil(t, disableFlag, "The --%s flag", cli.FlagDisable) { + expUsage := "Set the market's unittest field to false" + actusage := disableFlag.Usage + assert.Equal(t, expUsage, actusage, "--%s flag usage", cli.FlagDisable) + actAnnotations := disableFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "--%s flag annotations", cli.FlagDisable) + } +} + +func TestReadFlagsEnableDisable(t *testing.T) { + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Bool(cli.FlagEnable, false, "Enable") + flagSet.Bool(cli.FlagDisable, false, "Disable") + return flagSet + } + + tests := []struct { + name string + flags []string + flagSet func() *pflag.FlagSet + exp bool + expErr string + }{ + { + name: "cannot read enable", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Int(cli.FlagEnable, 0, "Enable") + flagSet.Bool(cli.FlagDisable, false, "Disable") + return flagSet + }, + expErr: "trying to get bool value of flag of type int", + }, + { + name: "cannot read disable", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Bool(cli.FlagEnable, false, "Enable") + flagSet.Int(cli.FlagDisable, 0, "Disable") + return flagSet + }, + expErr: "trying to get bool value of flag of type int", + }, + { + name: "enable", + flags: []string{"--" + cli.FlagEnable}, + flagSet: goodFlagSet, + exp: true, + }, + { + name: "disable", + flags: []string{"--" + cli.FlagDisable}, + flagSet: goodFlagSet, + exp: false, + }, + { + name: "neither", + flagSet: goodFlagSet, + expErr: "exactly one of --enable or --disable must be provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + flagSet := tc.flagSet() + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var act bool + testFunc := func() { + act, err = cli.ReadFlagsEnableDisable(flagSet) + } + require.NotPanics(t, testFunc, "ReadFlagsEnableDisable") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagsEnableDisable error") + assert.Equal(t, tc.exp, act, "ReadFlagsEnableDisable bool") + }) + } +} + +func TestAddFlagsAsksBidsBools(t *testing.T) { + expAnnotations := map[string][]string{ + mutExc: {cli.FlagAsks + " " + cli.FlagBids}, + } + + cmd := &cobra.Command{ + Use: "testing", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + cli.AddFlagsAsksBidsBools(cmd) + + asksFlag := cmd.Flags().Lookup(cli.FlagAsks) + if assert.NotNil(t, asksFlag, "The --%s flag", cli.FlagAsks) { + expUsage := "Limit results to only ask orders" + actusage := asksFlag.Usage + assert.Equal(t, expUsage, actusage, "--%s flag usage", cli.FlagAsks) + actAnnotations := asksFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "--%s flag annotations", cli.FlagAsks) + } + + bidsFlag := cmd.Flags().Lookup(cli.FlagBids) + if assert.NotNil(t, bidsFlag, "The --%s flag", cli.FlagBids) { + expUsage := "Limit results to only bid orders" + actusage := bidsFlag.Usage + assert.Equal(t, expUsage, actusage, "--%s flag usage", cli.FlagBids) + actAnnotations := bidsFlag.Annotations + assert.Equal(t, expAnnotations, actAnnotations, "--%s flag annotations", cli.FlagBids) + } +} + +func TestReadFlagsAsksBidsOpt(t *testing.T) { + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Bool(cli.FlagAsks, false, "Asks") + flagSet.Bool(cli.FlagBids, false, "Bids") + return flagSet + } + + tests := []struct { + name string + flags []string + flagSet func() *pflag.FlagSet + expStr string + expErr string + }{ + { + name: "cannot read asks", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Int(cli.FlagAsks, 0, "Asks") + flagSet.Bool(cli.FlagBids, false, "Bids") + return flagSet + }, + expErr: "trying to get bool value of flag of type int", + }, + { + name: "cannot read bids", + flagSet: func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Bool(cli.FlagAsks, false, "Asks") + flagSet.Int(cli.FlagBids, 0, "Bids") + return flagSet + }, + expErr: "trying to get bool value of flag of type int", + }, + { + name: "asks", + flags: []string{"--" + cli.FlagAsks}, + flagSet: goodFlagSet, + expStr: "ask", + }, + { + name: "bids", + flags: []string{"--" + cli.FlagBids}, + flagSet: goodFlagSet, + expStr: "bid", + }, + { + name: "neither", + flagSet: goodFlagSet, + expStr: "", + expErr: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + flagSet := tc.flagSet() + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var str string + testFunc := func() { + str, err = cli.ReadFlagsAsksBidsOpt(flagSet) + } + require.NotPanics(t, testFunc, "ReadFlagsAsksBidsOpt") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagsAsksBidsOpt error") + assert.Equal(t, tc.expStr, str, "ReadFlagsAsksBidsOpt string") + }) + } +} + +func TestReadFlagOrderOrArg(t *testing.T) { + theFlag := cli.FlagOrder + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Uint64(theFlag, 0, "The id") + return flagSet + } + badFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(theFlag, "", "The id") + return flagSet + } + + tests := []struct { + name string + flags []string + flagSet *pflag.FlagSet + args []string + expID uint64 + expErr string + }{ + { + name: "unknown flag", + flagSet: pflag.NewFlagSet("", pflag.ContinueOnError), + expErr: "flag accessed but not defined: " + theFlag, + }, + { + name: "wrong flag type", + flagSet: badFlagSet(), + expErr: "trying to get uint64 value of flag of type string", + }, + { + name: "both flag and arg", + flags: []string{"--" + theFlag, "8"}, + flagSet: goodFlagSet(), + args: []string{"8"}, + expErr: "cannot provide as both an arg (\"8\") and flag (--order 8)", + }, + { + name: "just flag", + flags: []string{"--" + theFlag, "8"}, + flagSet: goodFlagSet(), + expID: 8, + }, + { + name: "just flag zero", + flags: []string{"--" + theFlag, "0"}, + flagSet: goodFlagSet(), + expErr: "no provided", + }, + { + name: "just arg, bad", + flagSet: goodFlagSet(), + args: []string{"8v8"}, + expErr: "could not convert arg: strconv.ParseUint: parsing \"8v8\": invalid syntax", + }, + { + name: "just arg, zero", + flagSet: goodFlagSet(), + args: []string{"0"}, + expErr: "no provided", + }, + { + name: "just arg, good", + flagSet: goodFlagSet(), + args: []string{"987"}, + expID: 987, + }, + { + name: "neither flag nor arg", + flagSet: goodFlagSet(), + expErr: "no provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var id uint64 + testFunc := func() { + id, err = cli.ReadFlagOrderOrArg(tc.flagSet, tc.args) + } + require.NotPanics(t, testFunc, "ReadFlagOrderOrArg") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagOrderOrArg error") + assert.Equal(t, int(tc.expID), int(id), "ReadFlagOrderOrArg id") + }) + } +} + +func TestReadFlagMarketOrArg(t *testing.T) { + theFlag := cli.FlagMarket + goodFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Uint32(theFlag, 0, "The id") + return flagSet + } + badFlagSet := func() *pflag.FlagSet { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(theFlag, "", "The id") + return flagSet + } + + tests := []struct { + name string + flags []string + flagSet *pflag.FlagSet + args []string + expID uint32 + expErr string + }{ + { + name: "unknown flag", + flagSet: pflag.NewFlagSet("", pflag.ContinueOnError), + expErr: "flag accessed but not defined: " + theFlag, + }, + { + name: "wrong flag type", + flagSet: badFlagSet(), + expErr: "trying to get uint32 value of flag of type string", + }, + { + name: "both flag and arg", + flags: []string{"--" + theFlag, "8"}, + flagSet: goodFlagSet(), + args: []string{"8"}, + expErr: "cannot provide as both an arg (\"8\") and flag (--market 8)", + }, + { + name: "just flag", + flags: []string{"--" + theFlag, "8"}, + flagSet: goodFlagSet(), + expID: 8, + }, + { + name: "just flag zero", + flags: []string{"--" + theFlag, "0"}, + flagSet: goodFlagSet(), + expErr: "no provided", + }, + { + name: "just arg, bad", + flagSet: goodFlagSet(), + args: []string{"8v8"}, + expErr: "could not convert arg: strconv.ParseUint: parsing \"8v8\": invalid syntax", + }, + { + name: "just arg, zero", + flagSet: goodFlagSet(), + args: []string{"0"}, + expErr: "no provided", + }, + { + name: "just arg, good", + flagSet: goodFlagSet(), + args: []string{"987"}, + expID: 987, + }, + { + name: "neither flag nor arg", + flagSet: goodFlagSet(), + expErr: "no provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var id uint32 + testFunc := func() { + id, err = cli.ReadFlagMarketOrArg(tc.flagSet, tc.args) + } + require.NotPanics(t, testFunc, "ReadFlagMarketOrArg") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagMarketOrArg error") + assert.Equal(t, int(tc.expID), int(id), "ReadFlagMarketOrArg id") + }) + } +} + +func TestReadCoinsFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + expCoins sdk.Coins + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get string value of flag of type int", + }, + { + testName: "nothing provided", + name: flagString, + expErr: "", + }, + { + testName: "invalid coins", + flags: []string{"--" + flagString, "2yupcoin,nopecoin"}, + name: flagString, + expErr: "error parsing --" + flagString + " as coins: invalid coin expression: \"nopecoin\"", + }, + { + testName: "one coin", + flags: []string{"--" + flagString, "2grape"}, + name: flagString, + expCoins: sdk.NewCoins(sdk.NewInt64Coin("grape", 2)), + }, + { + testName: "three coins", + flags: []string{"--" + flagString, "8banana,5apple,14cherry"}, + name: flagString, + expCoins: sdk.NewCoins( + sdk.NewInt64Coin("apple", 5), sdk.NewInt64Coin("banana", 8), sdk.NewInt64Coin("cherry", 14), + ), + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var coins sdk.Coins + testFunc := func() { + coins, err = cli.ReadCoinsFlag(flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadCoinsFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadCoinsFlag(%q) error", tc.name) + assert.Equal(t, tc.expCoins.String(), coins.String(), "ReadCoinsFlag(%q) coins", tc.name) + }) + } +} + +func TestParseCoins(t *testing.T) { + tests := []struct { + name string + coinsStr string + expCoins sdk.Coins + expErr string + }{ + { + name: "empty string", + coinsStr: "", + expCoins: nil, + expErr: "", + }, + { + name: "one entry, bad", + coinsStr: "bad", + expErr: "invalid coin expression: \"bad\"", + }, + { + name: "one entry, good", + coinsStr: "55good", + expCoins: sdk.NewCoins(sdk.NewInt64Coin("good", 55)), + }, + { + name: "three entries, first bad", + coinsStr: "1234,555second,63third", + expErr: "invalid coin expression: \"1234\"", + }, + { + name: "three entries, second bad", + coinsStr: "1234first,second,55third", + expErr: "invalid coin expression: \"second\"", + }, + { + name: "three entries, third bad", + coinsStr: "1234first,555second,63x", + expErr: "invalid coin expression: \"63x\"", + }, + { + name: "three entries, all good", + coinsStr: "1234one,555two,63three", + expCoins: sdk.NewCoins( + sdk.NewInt64Coin("one", 1234), + sdk.NewInt64Coin("three", 63), + sdk.NewInt64Coin("two", 555), + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var coins sdk.Coins + var err error + testFunc := func() { + coins, err = cli.ParseCoins(tc.coinsStr) + } + require.NotPanics(t, testFunc, "ParseCoins(%q)", tc.coinsStr) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseCoins(%q) error", tc.coinsStr) + assert.Equal(t, tc.expCoins.String(), coins.String(), "ParseCoins(%q) coins", tc.coinsStr) + }) + } +} + +func TestReadCoinFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + expCoin *sdk.Coin + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get string value of flag of type int", + }, + { + testName: "nothing provided", + name: flagString, + expErr: "", + }, + { + testName: "invalid coin", + flags: []string{"--" + flagString, "nopecoin"}, + name: flagString, + expErr: "error parsing --" + flagString + " as a coin: invalid coin expression: \"nopecoin\"", + }, + { + testName: "zero coin", + flags: []string{"--" + flagString, "0zerocoin"}, + name: flagString, + expCoin: &sdk.Coin{Denom: "zerocoin", Amount: sdkmath.NewInt(0)}, + }, + { + testName: "normal coin", + flags: []string{"--" + flagString, "99banana"}, + name: flagString, + expCoin: &sdk.Coin{Denom: "banana", Amount: sdkmath.NewInt(99)}, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var coin *sdk.Coin + testFunc := func() { + coin, err = cli.ReadCoinFlag(flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadCoinFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadCoinFlag(%q) error", tc.name) + if !assert.Equal(t, tc.expCoin, coin, "ReadCoinFlag(%q)", tc.name) && tc.expCoin != nil && coin != nil { + t.Logf("Expected: %q", tc.expCoin) + t.Logf(" Actual: %q", coin) + } + }) + } +} + +func TestReadReqCoinFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + expCoin sdk.Coin + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get string value of flag of type int", + }, + { + testName: "nothing provided", + name: flagString, + expErr: "missing required --" + flagString + " flag", + }, + { + testName: "invalid coin", + flags: []string{"--" + flagString, "nopecoin"}, + name: flagString, + expErr: "error parsing --" + flagString + " as a coin: invalid coin expression: \"nopecoin\"", + }, + { + testName: "zero coin", + flags: []string{"--" + flagString, "0zerocoin"}, + name: flagString, + expCoin: sdk.Coin{Denom: "zerocoin", Amount: sdkmath.NewInt(0)}, + }, + { + testName: "normal coin", + flags: []string{"--" + flagString, "99banana"}, + name: flagString, + expCoin: sdk.Coin{Denom: "banana", Amount: sdkmath.NewInt(99)}, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var coin sdk.Coin + testFunc := func() { + coin, err = cli.ReadReqCoinFlag(flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadReqCoinFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadReqCoinFlag(%q) error", tc.name) + assert.Equal(t, tc.expCoin.String(), coin.String(), "ReadReqCoinFlag(%q)", tc.name) + }) + } +} + +func TestReadOrderIDsFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + expIDs []uint64 + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagString, + expErr: "trying to get string value of flag of type uintSlice", + }, + { + testName: "nothing provided", + name: flagUintSlice, + expErr: "", + }, + { + testName: "one val", + flags: []string{"--" + flagUintSlice, "15"}, + name: flagUintSlice, + expIDs: []uint64{15}, + }, + { + testName: "three vals", + flags: []string{"--" + flagUintSlice, "42,9001,3"}, + name: flagUintSlice, + expIDs: []uint64{42, 9001, 3}, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.UintSlice(flagUintSlice, nil, "A slice of uints") + flagSet.String(flagString, "", "A string") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var ids []uint64 + testFunc := func() { + ids, err = cli.ReadOrderIDsFlag(flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadOrderIDsFlag(%q)", tc.name) + assertEqualSlices(t, tc.expIDs, ids, orderIDStringer, "ReadOrderIDsFlag(%q)", tc.name) + }) + } +} + +func TestReadAccessGrantsFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + def []exchange.AccessGrant + expGrants []exchange.AccessGrant + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get stringSlice value of flag of type int", + }, + { + testName: "nothing provided, nil default", + name: flagStringSlice, + expErr: "", + }, + { + testName: "nothing provided, with default", + name: flagStringSlice, + def: []exchange.AccessGrant{ + {Address: "someone", Permissions: []exchange.Permission{3, 4}}, + }, + expGrants: []exchange.AccessGrant{ + {Address: "someone", Permissions: []exchange.Permission{3, 4}}, + }, + expErr: "", + }, + { + testName: "three vals, one bad", + flags: []string{ + "--" + flagStringSlice, "addr1:all", + "--" + flagStringSlice, "withdraw", + "--" + flagStringSlice, "addr2:setids+update", + }, + name: flagStringSlice, + expGrants: []exchange.AccessGrant{ + { + Address: "addr1", + Permissions: exchange.AllPermissions(), + }, + { + Address: "addr2", + Permissions: []exchange.Permission{exchange.Permission_set_ids, exchange.Permission_update}, + }, + }, + expErr: "could not parse \"withdraw\" as an : expected format
:", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.StringSlice(flagStringSlice, nil, "A string slice") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var grants []exchange.AccessGrant + testFunc := func() { + grants, err = cli.ReadAccessGrantsFlag(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadAccessGrantsFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadAccessGrantsFlag(%q) error", tc.name) + assert.Equal(t, tc.expGrants, grants, "ReadAccessGrantsFlag(%q) grants", tc.name) + }) + } +} + +func TestParseAccessGrant(t *testing.T) { + addr := "pb1v9jxgujlta047h6lta047h6lta047h6l5rpeqp" // = sdk.AccAddress("addr________________") + + tests := []struct { + name string + val string + expAG *exchange.AccessGrant + expErr string + }{ + { + name: "empty string", + val: "", + expAG: nil, + expErr: "could not parse \"\" as an : expected format
:", + }, + { + name: "zero colons", + val: "something", + expErr: "could not parse \"something\" as an : expected format
:", + }, + { + name: "two colons", + val: "part0:part1:part2", + expErr: "could not parse \"part0:part1:part2\" as an : expected format
:", + }, + { + name: "empty address", + val: ":part1", + expErr: "invalid \":part1\": both an
and are required", + }, + { + name: "empty permissions", + val: "part0:", + expErr: "invalid \"part0:\": both an
and are required", + }, + { + name: "unspecified", + val: "part0:unspecified", + expErr: "could not parse permissions for \"part0\" from \"unspecified\": invalid permission: \"unspecified\"", + }, + { + name: "all", + val: addr + ":all", + expAG: &exchange.AccessGrant{Address: addr, Permissions: exchange.AllPermissions()}, + }, + { + name: "one perm, enum name", + val: addr + ":PERMISSION_UPDATE", + expAG: &exchange.AccessGrant{ + Address: addr, + Permissions: []exchange.Permission{exchange.Permission_update}, + }, + }, + { + name: "one perm, simple name", + val: addr + ":cancel", + expAG: &exchange.AccessGrant{ + Address: addr, + Permissions: []exchange.Permission{exchange.Permission_cancel}, + }, + }, + { + name: "multiple perms, plus delim", + val: addr + ":Cancel+PERMISSION_SETTLE+setids", + expAG: &exchange.AccessGrant{ + Address: addr, + Permissions: []exchange.Permission{ + exchange.Permission_cancel, + exchange.Permission_settle, + exchange.Permission_set_ids, + }, + }, + }, + { + name: "multiple perms, dot delim", + val: addr + ":permissions.PERMISSION_ATTRIBUTES.withdraw", + expAG: &exchange.AccessGrant{ + Address: addr, + Permissions: []exchange.Permission{ + exchange.Permission_permissions, + exchange.Permission_attributes, + exchange.Permission_withdraw, + }, + }, + }, + { + name: "multiple perms, space delim", + val: addr + ":Set_Ids update settle permissions", + expAG: &exchange.AccessGrant{ + Address: addr, + Permissions: []exchange.Permission{ + exchange.Permission_set_ids, + exchange.Permission_update, + exchange.Permission_settle, + exchange.Permission_permissions, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var ag *exchange.AccessGrant + var err error + testFunc := func() { + ag, err = cli.ParseAccessGrant(tc.val) + } + require.NotPanics(t, testFunc, "ParseAccessGrant(%q)", tc.val) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseAccessGrant(%q) error", tc.val) + assert.Equal(t, tc.expAG, ag, "ParseAccessGrant(%q) AccessGrant", tc.val) + }) + } +} + +func TestParseAccessGrants(t *testing.T) { + addr1 := "pb1v9jxgu33ta047h6lta047h6lta047h6l0r6x5v" // = sdk.AccAddress("addr1_______________").String() + addr2 := "pb1taskgerjxf047h6lta047h6lta047h6lrcgmd9" // = sdk.AccAddress("_addr2______________").String() + addr3 := "pb10elxzerywge47h6lta047h6lta047h6l90x0zx" // = sdk.AccAddress("~~addr3_____________").String() + + tests := []struct { + name string + vals []string + expGrants []exchange.AccessGrant + expErr string + }{ + { + name: "nil", + vals: nil, + expErr: "", + }, + { + name: "empty", + vals: []string{}, + expErr: "", + }, + { + name: "one, bad", + vals: []string{"not good"}, + expErr: "could not parse \"not good\" as an : expected format
:", + }, + { + name: "one, good", + vals: []string{addr1 + ":update+permissions"}, + expGrants: []exchange.AccessGrant{{ + Address: addr1, + Permissions: []exchange.Permission{exchange.Permission_update, exchange.Permission_permissions}, + }}, + }, + { + name: "three, all good", + vals: []string{addr1 + ":settle", addr2 + ":setids", addr3 + ":permission_withdraw"}, + expGrants: []exchange.AccessGrant{ + {Address: addr1, Permissions: []exchange.Permission{exchange.Permission_settle}}, + {Address: addr2, Permissions: []exchange.Permission{exchange.Permission_set_ids}}, + {Address: addr3, Permissions: []exchange.Permission{exchange.Permission_withdraw}}, + }, + }, + { + name: "three, first bad", + vals: []string{":settle", addr2 + ":setids", addr3 + ":permission_withdraw"}, + expGrants: []exchange.AccessGrant{ + {Address: addr2, Permissions: []exchange.Permission{exchange.Permission_set_ids}}, + {Address: addr3, Permissions: []exchange.Permission{exchange.Permission_withdraw}}, + }, + expErr: "invalid \":settle\": both an
and are required", + }, + { + name: "three, second bad", + vals: []string{addr1 + ":settle", addr2 + ":unspecified", addr3 + ":permission_withdraw"}, + expGrants: []exchange.AccessGrant{ + {Address: addr1, Permissions: []exchange.Permission{exchange.Permission_settle}}, + {Address: addr3, Permissions: []exchange.Permission{exchange.Permission_withdraw}}, + }, + expErr: "could not parse permissions for \"" + addr2 + "\" from \"unspecified\": invalid permission: \"unspecified\"", + }, + { + name: "three, third bad", + vals: []string{addr1 + ":settle", addr2 + ":setids", "someaddr:"}, + expGrants: []exchange.AccessGrant{ + {Address: addr1, Permissions: []exchange.Permission{exchange.Permission_settle}}, + {Address: addr2, Permissions: []exchange.Permission{exchange.Permission_set_ids}}, + }, + expErr: "invalid \"someaddr:\": both an
and are required", + }, + { + name: "three, all bad", + vals: []string{":settle", addr2 + ":unspecified", "someaddr:"}, + expErr: joinErrs( + "invalid \":settle\": both an
and are required", + "could not parse permissions for \""+addr2+"\" from \"unspecified\": invalid permission: \"unspecified\"", + "invalid \"someaddr:\": both an
and are required", + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expGrants == nil { + tc.expGrants = []exchange.AccessGrant{} + } + + var grants []exchange.AccessGrant + var err error + testFunc := func() { + grants, err = cli.ParseAccessGrants(tc.vals) + } + require.NotPanics(t, testFunc, "ParseAccessGrants(%q)", tc.vals) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseAccessGrants(%q) error", tc.vals) + assert.Equal(t, tc.expGrants, grants, "ParseAccessGrants(%q) grants", tc.vals) + }) + } +} + +func TestReadFlatFeeFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + def []sdk.Coin + expCoins []sdk.Coin + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get stringSlice value of flag of type int", + }, + { + testName: "nothing provided, nil default", + name: flagStringSlice, + expErr: "", + }, + { + testName: "nothing provided, with default", + name: flagStringSlice, + def: []sdk.Coin{sdk.NewInt64Coin("cherry", 123)}, + expCoins: []sdk.Coin{sdk.NewInt64Coin("cherry", 123)}, + expErr: "", + }, + { + testName: "three vals, one bad", + flags: []string{"--" + flagStringSlice, "apple,100pear", "--" + flagStringSlice, "777cherry"}, + name: flagStringSlice, + expCoins: []sdk.Coin{sdk.NewInt64Coin("pear", 100), sdk.NewInt64Coin("cherry", 777)}, + expErr: "invalid coin expression: \"apple\"", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.StringSlice(flagStringSlice, nil, "A string slice") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var coins []sdk.Coin + testFunc := func() { + coins, err = cli.ReadFlatFeeFlag(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlatFeeFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlatFeeFlag(%q) error", tc.name) + assertEqualSlices(t, tc.expCoins, coins, sdk.Coin.String, "ReadFlatFeeFlag(%q) ratios", tc.name) + }) + } +} + +func TestParseFlatFeeOptions(t *testing.T) { + tests := []struct { + name string + vals []string + expCoins []sdk.Coin + expErr string + }{ + { + name: "nil", + vals: nil, + expErr: "", + }, + { + name: "empty", + vals: []string{}, + expErr: "", + }, + { + name: "one, bad", + vals: []string{"nope"}, + expErr: "invalid coin expression: \"nope\"", + }, + { + name: "one, good", + vals: []string{"18banana"}, + expCoins: []sdk.Coin{sdk.NewInt64Coin("banana", 18)}, + }, + { + name: "one, zero", + vals: []string{"0durian"}, + expCoins: []sdk.Coin{sdk.NewInt64Coin("durian", 0)}, + }, + { + name: "three, all good", + vals: []string{"1apple", "2banana", "3cherry"}, + expCoins: []sdk.Coin{ + sdk.NewInt64Coin("apple", 1), sdk.NewInt64Coin("banana", 2), sdk.NewInt64Coin("cherry", 3), + }, + }, + { + name: "three, first bad", + vals: []string{"notgonnacoin", "2banana", "3cherry"}, + expCoins: []sdk.Coin{ + sdk.NewInt64Coin("banana", 2), sdk.NewInt64Coin("cherry", 3), + }, + expErr: "invalid coin expression: \"notgonnacoin\"", + }, + { + name: "three, second bad", + vals: []string{"1apple", "12345", "3cherry"}, + expCoins: []sdk.Coin{ + sdk.NewInt64Coin("apple", 1), sdk.NewInt64Coin("cherry", 3), + }, + expErr: "invalid coin expression: \"12345\"", + }, + { + name: "three, third bad", + vals: []string{"1apple", "2banana", ""}, + expCoins: []sdk.Coin{ + sdk.NewInt64Coin("apple", 1), sdk.NewInt64Coin("banana", 2), + }, + expErr: "invalid coin expression: \"\"", + }, + { + name: "three, all bad", + vals: []string{"notgonnacoin", "12345", ""}, + expErr: joinErrs( + "invalid coin expression: \"notgonnacoin\"", + "invalid coin expression: \"12345\"", + "invalid coin expression: \"\"", + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expCoins == nil { + tc.expCoins = []sdk.Coin{} + } + var coins []sdk.Coin + var err error + testFunc := func() { + coins, err = cli.ParseFlatFeeOptions(tc.vals) + } + require.NotPanics(t, testFunc, "ParseFlatFeeOptions(%q)", tc.vals) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseFlatFeeOptions(%q) error", tc.vals) + assertEqualSlices(t, tc.expCoins, coins, sdk.Coin.String, "ParseFlatFeeOptions(%q) coins", tc.vals) + }) + } +} + +func TestReadFeeRatiosFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + def []exchange.FeeRatio + expRatios []exchange.FeeRatio + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get stringSlice value of flag of type int", + }, + { + testName: "nothing provided, nil default", + name: flagStringSlice, + expErr: "", + }, + { + testName: "nothing provided, with default", + name: flagStringSlice, + def: []exchange.FeeRatio{{Price: sdk.NewInt64Coin("apple", 500), Fee: sdk.NewInt64Coin("plum", 3)}}, + expRatios: []exchange.FeeRatio{{Price: sdk.NewInt64Coin("apple", 500), Fee: sdk.NewInt64Coin("plum", 3)}}, + expErr: "", + }, + { + testName: "three vals, one bad", + flags: []string{"--" + flagStringSlice, "8apple:3apple,100pear:1apple", "--" + flagStringSlice, "cherry:777cherry"}, + name: flagStringSlice, + expRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("apple", 8), Fee: sdk.NewInt64Coin("apple", 3)}, + {Price: sdk.NewInt64Coin("pear", 100), Fee: sdk.NewInt64Coin("apple", 1)}, + }, + expErr: "cannot create FeeRatio from \"cherry:777cherry\": price: invalid coin expression: \"cherry\"", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.StringSlice(flagStringSlice, nil, "A string slice") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var ratios []exchange.FeeRatio + testFunc := func() { + ratios, err = cli.ReadFeeRatiosFlag(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFeeRatiosFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFeeRatiosFlag(%q) error", tc.name) + assertEqualSlices(t, tc.expRatios, ratios, exchange.FeeRatio.String, "ReadFeeRatiosFlag(%q) ratios", tc.name) + }) + } +} + +func TestParseFeeRatios(t *testing.T) { + tests := []struct { + name string + vals []string + expRatios []exchange.FeeRatio + expErr string + }{ + { + name: "nil", + vals: nil, + expErr: "", + }, + { + name: "empty", + vals: []string{}, + expErr: "", + }, + { + name: "one, bad", + vals: []string{"notaratio"}, + expErr: "cannot create FeeRatio from \"notaratio\": expected exactly one colon", + }, + { + name: "one, good", + vals: []string{"10apple:3banana"}, + expRatios: []exchange.FeeRatio{{Price: sdk.NewInt64Coin("apple", 10), Fee: sdk.NewInt64Coin("banana", 3)}}, + }, + { + name: "one, zeros", + vals: []string{"0cherry:0durian"}, + expRatios: []exchange.FeeRatio{{Price: sdk.NewInt64Coin("cherry", 0), Fee: sdk.NewInt64Coin("durian", 0)}}, + }, + { + name: "three, all good", + vals: []string{"10apple:1cherry", "321banana:8grape", "66plum:7plum"}, + expRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("apple", 10), Fee: sdk.NewInt64Coin("cherry", 1)}, + {Price: sdk.NewInt64Coin("banana", 321), Fee: sdk.NewInt64Coin("grape", 8)}, + {Price: sdk.NewInt64Coin("plum", 66), Fee: sdk.NewInt64Coin("plum", 7)}, + }, + }, + { + name: "three, first bad", + vals: []string{"10apple", "321banana:8grape", "66plum:7plum"}, + expRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("banana", 321), Fee: sdk.NewInt64Coin("grape", 8)}, + {Price: sdk.NewInt64Coin("plum", 66), Fee: sdk.NewInt64Coin("plum", 7)}, + }, + expErr: "cannot create FeeRatio from \"10apple\": expected exactly one colon", + }, + { + name: "three, second bad", + vals: []string{"10apple:1cherry", "8grape", "66plum:7plum"}, + expRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("apple", 10), Fee: sdk.NewInt64Coin("cherry", 1)}, + {Price: sdk.NewInt64Coin("plum", 66), Fee: sdk.NewInt64Coin("plum", 7)}, + }, + expErr: "cannot create FeeRatio from \"8grape\": expected exactly one colon", + }, + { + name: "three, third bad", + vals: []string{"10apple:1cherry", "321banana:8grape", ""}, + expRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("apple", 10), Fee: sdk.NewInt64Coin("cherry", 1)}, + {Price: sdk.NewInt64Coin("banana", 321), Fee: sdk.NewInt64Coin("grape", 8)}, + }, + expErr: "cannot create FeeRatio from \"\": expected exactly one colon", + }, + { + name: "three, all bad", + vals: []string{"10apple", "8grape", ""}, + expErr: joinErrs( + "cannot create FeeRatio from \"10apple\": expected exactly one colon", + "cannot create FeeRatio from \"8grape\": expected exactly one colon", + "cannot create FeeRatio from \"\": expected exactly one colon", + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expRatios == nil { + tc.expRatios = []exchange.FeeRatio{} + } + + var ratios []exchange.FeeRatio + var err error + testFunc := func() { + ratios, err = cli.ParseFeeRatios(tc.vals) + } + require.NotPanics(t, testFunc, "ParseFeeRatios(%q)", tc.vals) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseFeeRatios(%q) error", tc.vals) + assertEqualSlices(t, tc.expRatios, ratios, exchange.FeeRatio.String, "ParseFeeRatios(%q) ratios", tc.vals) + }) + } +} + +func TestReadSplitsFlag(t *testing.T) { + tests := []struct { + testName string + flags []string + name string + expSplits []exchange.DenomSplit + expErr string + }{ + { + testName: "unknown flag", + name: "unknown", + expErr: "flag accessed but not defined: unknown", + }, + { + testName: "wrong flag type", + name: flagInt, + expErr: "trying to get stringSlice value of flag of type int", + }, + { + testName: "nothing provided", + name: flagStringSlice, + expErr: "", + }, + { + testName: "three vals, one bad", + flags: []string{"--" + flagStringSlice, "apple:3,banana:80q0", "--" + flagStringSlice, "cherry:777"}, + name: flagStringSlice, + expSplits: []exchange.DenomSplit{ + {Denom: "apple", Split: 3}, + {Denom: "cherry", Split: 777}, + }, + expErr: "could not parse \"banana:80q0\" amount: strconv.ParseUint: parsing \"80q0\": invalid syntax", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.StringSlice(flagStringSlice, nil, "A string slice") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var splits []exchange.DenomSplit + testFunc := func() { + splits, err = cli.ReadSplitsFlag(flagSet, tc.name) + } + require.NotPanics(t, testFunc, "ReadSplitsFlag(%q)", tc.name) + assertions.AssertErrorValue(t, err, tc.expErr, "ReadSplitsFlag(%q) error", tc.name) + assertEqualSlices(t, tc.expSplits, splits, splitStringer, "ReadSplitsFlag(%q) splits", tc.name) + }) + } +} + +func TestParseSplit(t *testing.T) { + tests := []struct { + name string + val string + expSplit *exchange.DenomSplit + expErr string + }{ + { + name: "empty", + val: "", + expErr: "invalid denom split \"\": expected format :", + }, + { + name: "no colons", + val: "banana", + expSplit: nil, + expErr: "invalid denom split \"banana\": expected format :", + }, + { + name: "two colons", + val: "plum:8:123", + expErr: "invalid denom split \"plum:8:123\": expected format :", + }, + { + name: "empty denom", + val: ":444", + expErr: "invalid denom split \":444\": both a and are required", + }, + { + name: "empty amount", + val: "apple:", + expErr: "invalid denom split \"apple:\": both a and are required", + }, + { + name: "invalid amount", + val: "apple:banana", + expErr: "could not parse \"apple:banana\" amount: strconv.ParseUint: parsing \"banana\": invalid syntax", + }, + { + name: "good, zero", + val: "cherry:0", + expSplit: &exchange.DenomSplit{Denom: "cherry", Split: 0}, + }, + { + name: "good, 10,000", + val: "pear:10000", + expSplit: &exchange.DenomSplit{Denom: "pear", Split: 10000}, + }, + { + name: "good, 123", + val: "acorn:123", + expSplit: &exchange.DenomSplit{Denom: "acorn", Split: 123}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var split *exchange.DenomSplit + var err error + testFunc := func() { + split, err = cli.ParseSplit(tc.val) + } + require.NotPanics(t, testFunc, "ParseSplit(%q)", tc.val) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseSplit(%q) error", tc.val) + if !assert.Equal(t, tc.expSplit, split, "ParseSplit(%q) split", tc.val) { + t.Logf("Expected: %s:%d", tc.expSplit.Denom, tc.expSplit.Split) + t.Logf(" Actual: %s:%d", split.Denom, split.Split) + } + }) + } +} + +func TestParseSplits(t *testing.T) { + tests := []struct { + name string + vals []string + expSplits []exchange.DenomSplit + expErr string + }{ + { + name: "nil", + vals: nil, + expErr: "", + }, + { + name: "empty", + vals: []string{}, + expErr: "", + }, + { + name: "one, bad", + vals: []string{"nope"}, + expErr: "invalid denom split \"nope\": expected format :", + }, + { + name: "one, good", + vals: []string{"yup:5"}, + expSplits: []exchange.DenomSplit{{Denom: "yup", Split: 5}}, + }, + { + name: "three, all good", + vals: []string{"first:1", "second:22", "third:333"}, + expSplits: []exchange.DenomSplit{ + {Denom: "first", Split: 1}, {Denom: "second", Split: 22}, {Denom: "third", Split: 333}, + }, + }, + { + name: "three, first bad", + vals: []string{"first", "second:22", "third:333"}, + expSplits: []exchange.DenomSplit{ + {Denom: "second", Split: 22}, {Denom: "third", Split: 333}, + }, + expErr: "invalid denom split \"first\": expected format :", + }, + { + name: "three, second bad", + vals: []string{"first:1", ":22", "third:333"}, + expSplits: []exchange.DenomSplit{ + {Denom: "first", Split: 1}, {Denom: "third", Split: 333}, + }, + expErr: "invalid denom split \":22\": both a and are required", + }, + { + name: "three, third bad", + vals: []string{"first:1", "second:22", "third:333x"}, + expSplits: []exchange.DenomSplit{ + {Denom: "first", Split: 1}, {Denom: "second", Split: 22}, + }, + expErr: "could not parse \"third:333x\" amount: strconv.ParseUint: parsing \"333x\": invalid syntax", + }, + { + name: "three, all bad", + vals: []string{"first", ":22", "third:333x"}, + expErr: joinErrs( + "invalid denom split \"first\": expected format :", + "invalid denom split \":22\": both a and are required", + "could not parse \"third:333x\" amount: strconv.ParseUint: parsing \"333x\": invalid syntax", + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.expSplits == nil { + tc.expSplits = []exchange.DenomSplit{} + } + + var splits []exchange.DenomSplit + var err error + testFunc := func() { + splits, err = cli.ParseSplits(tc.vals) + } + require.NotPanics(t, testFunc, "ParseSplits(%q)", tc.vals) + assertions.AssertErrorValue(t, err, tc.expErr, "ParseSplits(%q) error", tc.vals) + assertEqualSlices(t, tc.expSplits, splits, splitStringer, "ParseSplits(%q) splits", tc.vals) + }) + } +} + +func TestReadStringFlagOrArg(t *testing.T) { + tests := []struct { + name string + flags []string + args []string + flagName string + varName string + expStr string + expErr string + }{ + { + name: "unknown flag name", + flagName: "other", + varName: "nope", + expErr: "flag accessed but not defined: other", + }, + { + name: "wrong flag type", + flagName: flagInt, + varName: "number", + expErr: "trying to get string value of flag of type int", + }, + { + name: "both flag and arg", + flags: []string{"--" + flagString, "flagval"}, + args: []string{"argval"}, + flagName: flagString, + varName: "value", + expErr: "cannot provide as both an arg (\"argval\") and flag (--" + flagString + " \"flagval\")", + }, + { + name: "only flag", + flags: []string{"--" + flagString, "flagval"}, + flagName: flagString, + varName: "value", + expStr: "flagval", + }, + { + name: "only arg", + args: []string{"argval"}, + flagName: flagString, + varName: "value", + expStr: "argval", + }, + { + name: "neither flag nor arg", + flagName: flagString, + varName: "value", + expErr: "no provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var str string + testFunc := func() { + str, err = cli.ReadStringFlagOrArg(flagSet, tc.args, tc.flagName, tc.varName) + } + require.NotPanics(t, testFunc, "ReadStringFlagOrArg") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadStringFlagOrArg error") + assert.Equal(t, tc.expStr, str, "ReadStringFlagOrArg string") + }) + } +} + +func TestReadProposalFlag(t *testing.T) { + tests := []struct { + name string + // setup should return the proposal filename and the expected Anys. + setup func(t *testing.T) (string, []*codectypes.Any) + flagSet *pflag.FlagSet + expInErr []string + }{ + { + name: "err getting flag", + setup: func(t *testing.T) (string, []*codectypes.Any) { + return "", nil + }, + flagSet: pflag.NewFlagSet("", pflag.ContinueOnError), + expInErr: []string{"flag accessed but not defined: proposal"}, + }, + { + name: "no flag given", + setup: func(t *testing.T) (string, []*codectypes.Any) { + return "", nil + }, + expInErr: nil, + }, + { + name: "file does not exist", + setup: func(t *testing.T) (string, []*codectypes.Any) { + tdir := t.TempDir() + noSuchFile := filepath.Join(tdir, "no-such-file.json") + return noSuchFile, nil + }, + expInErr: []string{"open ", "no-such-file.json", "no such file or directory"}, + }, + { + name: "cannot unmarshal contents", + setup: func(t *testing.T) (string, []*codectypes.Any) { + tdir := t.TempDir() + notJSON := filepath.Join(tdir, "not-json.json") + contents := []byte("This is not\na JSON file.\n") + writeFile(t, notJSON, contents) + return notJSON, nil + }, + expInErr: []string{ + "failed to unmarshal --proposal \"", "\" contents as Tx", + "invalid character 'T' looking for beginning of value", + }, + }, + { + name: "no body", + setup: func(t *testing.T) (string, []*codectypes.Any) { + contents := `{ + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "200000", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] +} +` + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-body.json") + writeFile(t, fn, []byte(contents)) + return fn, nil + }, + expInErr: []string{"the contents of \"", "\" does not have a \"body\""}, + }, + { + name: "no body messages", + setup: func(t *testing.T) (string, []*codectypes.Any) { + tx := newTx(t) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-body-messages.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"the contents of \"", "\" does not have any body messages"}, + }, + { + name: "no submit proposals", + setup: func(t *testing.T) (string, []*codectypes.Any) { + msg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + MarketDetails: exchange.MarketDetails{ + Name: "New Market Name", + }, + }, + } + tx := newTx(t, msg) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-proposals.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"no *v1.MsgSubmitProposal messages found in \""}, + }, + { + name: "no messages in submit proposals", + setup: func(t *testing.T) (string, []*codectypes.Any) { + prop := newGovProp(t) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-messages-in-proposal.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"no messages found in any *v1.MsgSubmitProposal messages in \""}, + }, + { + name: "1 message found", + setup: func(t *testing.T) (string, []*codectypes.Any) { + msg := &exchange.MsgMarketWithdrawRequest{ + Admin: sdk.AccAddress("Admin_______________").String(), + MarketId: 3, + ToAddress: sdk.AccAddress("ToAddress___________").String(), + Amount: sdk.NewCoins(sdk.NewInt64Coin("apple", 15)), + } + prop := newGovProp(t, msg) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "one-message.json") + writeFileAsJson(t, fn, tx) + return fn, prop.Messages + }, + expInErr: nil, + }, + { + name: "3 messages found", + setup: func(t *testing.T) (string, []*codectypes.Any) { + msg1 := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{MarketId: 88}, + } + msg2 := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 42, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("plum", 5)}, + } + msg3 := &exchange.MsgCancelOrderRequest{ + Signer: cli.AuthorityAddr.String(), + OrderId: 5555, + } + prop1 := newGovProp(t, msg1) + prop2 := newGovProp(t, msg2, msg3) + tx := newTx(t, prop1, prop2) + tdir := t.TempDir() + fn := filepath.Join(tdir, "three-messages.json") + writeFileAsJson(t, fn, tx) + expAnys := make([]*codectypes.Any, 0, len(prop1.Messages)+len(prop2.Messages)) + expAnys = append(expAnys, prop1.Messages...) + expAnys = append(expAnys, prop2.Messages...) + return fn, expAnys + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + propFN, expAnys := tc.setup(t) + + if tc.flagSet == nil { + tc.flagSet = pflag.NewFlagSet("", pflag.ContinueOnError) + tc.flagSet.String(cli.FlagProposal, "", "The Proposal") + } + + if len(propFN) > 0 && len(tc.expInErr) > 0 { + tc.expInErr = append(tc.expInErr, propFN) + } + + args := make([]string, 0, 2) + if len(propFN) > 0 { + args = append(args, "--"+cli.FlagProposal, propFN) + } + + err := tc.flagSet.Parse(args) + require.NoError(t, err, "flagSet.Parse(%q)", args) + + clientCtx := newClientContextWithCodec() + + var actPropFN string + var actAnys []*codectypes.Any + testFunc := func() { + actPropFN, actAnys, err = cli.ReadProposalFlag(clientCtx, tc.flagSet) + } + require.NotPanics(t, testFunc, "ReadProposalFlag") + + assertions.AssertErrorContents(t, err, tc.expInErr, "ReadProposalFlag error") + assert.Equal(t, propFN, actPropFN, "ReadProposalFlag filename") + // We can't just assert that expAnys and actAnys are equal due to some internal differences. + // All we really care about is that they have the same types and msg contents. + expTypes := getAnyTypes(expAnys) + actTypes := getAnyTypes(actAnys) + if assert.Equal(t, expTypes, actTypes, "ReadProposalFlag anys types") { + for i := range expAnys { + expMsg := expAnys[i].GetCachedValue() + actMsg := actAnys[i].GetCachedValue() + assert.Equal(t, expMsg, actMsg, "ReadProposalFlag anys[%d] cached value", i) + } + } + }) + } +} + +func TestReadMsgGovCreateMarketRequestFromProposalFlag(t *testing.T) { + tests := []struct { + name string + // setup should return the proposal filename and the expected Msg. + setup func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) + expInErr []string + }{ + { + name: "error reading file", + setup: func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) { + tx := newTx(t) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-body-messages.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"the contents of \"", "\" does not have any body messages"}, + }, + { + name: "no flag given", + setup: func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) { + return "", nil + }, + expInErr: nil, + }, + { + name: "no msgs of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) { + msg := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 13, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 5000)}, + } + prop := newGovProp(t, msg) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"no *exchange.MsgGovCreateMarketRequest found in \""}, + }, + { + name: "two msgs of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) { + msg1 := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "Some Name"}}, + } + msg2 := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "Another Name"}}, + } + prop := newGovProp(t, msg1, msg2) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "two-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"2 *exchange.MsgGovCreateMarketRequest found in \""}, + }, + { + name: "one msg of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovCreateMarketRequest) { + msg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "The Only Name"}}, + } + prop := newGovProp(t, msg) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "two-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, msg + }, + expInErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + propFN, expected := tc.setup(t) + + if expected == nil { + expected = &exchange.MsgGovCreateMarketRequest{} + } + + if len(propFN) > 0 && len(tc.expInErr) > 0 { + tc.expInErr = append(tc.expInErr, propFN) + } + + args := make([]string, 0, 2) + if len(propFN) > 0 { + args = append(args, "--"+cli.FlagProposal, propFN) + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagProposal, "", "The Proposal") + err := flagSet.Parse(args) + require.NoError(t, err, "flagSet.Parse(%q)", args) + + clientCtx := newClientContextWithCodec() + + var actual *exchange.MsgGovCreateMarketRequest + testFunc := func() { + actual, err = cli.ReadMsgGovCreateMarketRequestFromProposalFlag(clientCtx, flagSet) + } + require.NotPanics(t, testFunc, "ReadMsgGovCreateMarketRequestFromProposalFlag") + assertions.AssertErrorContents(t, err, tc.expInErr, "ReadMsgGovCreateMarketRequestFromProposalFlag error") + assert.Equal(t, expected, actual, "ReadMsgGovCreateMarketRequestFromProposalFlag result") + }) + } +} + +func TestReadMsgGovManageFeesRequestFromProposalFlag(t *testing.T) { + tests := []struct { + name string + // setup should return the proposal filename and the expected Msg. + setup func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) + expInErr []string + }{ + { + name: "error reading file", + setup: func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) { + tx := newTx(t) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-body-messages.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"the contents of \"", "\" does not have any body messages"}, + }, + { + name: "no flag given", + setup: func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) { + return "", nil + }, + expInErr: nil, + }, + { + name: "no msgs of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) { + msg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{MarketDetails: exchange.MarketDetails{Name: "Some Name"}}, + } + prop := newGovProp(t, msg) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "no-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"no *exchange.MsgGovManageFeesRequest found in \""}, + }, + { + name: "two msgs of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) { + msg1 := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 12, + RemoveFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 99)}, + } + msg2 := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 13, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 5000)}, + } + prop := newGovProp(t, msg1, msg2) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "two-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, nil + }, + expInErr: []string{"2 *exchange.MsgGovManageFeesRequest found in \""}, + }, + { + name: "one msg of interest", + setup: func(t *testing.T) (string, *exchange.MsgGovManageFeesRequest) { + msg := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 2, + AddFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 8)}, + } + prop := newGovProp(t, msg) + tx := newTx(t, prop) + tdir := t.TempDir() + fn := filepath.Join(tdir, "two-messages-of-interest.json") + writeFileAsJson(t, fn, tx) + return fn, msg + }, + expInErr: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + propFN, expected := tc.setup(t) + + if expected == nil { + expected = &exchange.MsgGovManageFeesRequest{} + } + + if len(propFN) > 0 && len(tc.expInErr) > 0 { + tc.expInErr = append(tc.expInErr, propFN) + } + + args := make([]string, 0, 2) + if len(propFN) > 0 { + args = append(args, "--"+cli.FlagProposal, propFN) + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(cli.FlagProposal, "", "The Proposal") + err := flagSet.Parse(args) + require.NoError(t, err, "flagSet.Parse(%q)", args) + + clientCtx := newClientContextWithCodec() + + var actual *exchange.MsgGovManageFeesRequest + testFunc := func() { + actual, err = cli.ReadMsgGovManageFeesRequestFromProposalFlag(clientCtx, flagSet) + } + require.NotPanics(t, testFunc, "ReadMsgGovManageFeesRequestFromProposalFlag") + assertions.AssertErrorContents(t, err, tc.expInErr, "ReadMsgGovManageFeesRequestFromProposalFlag error") + assert.Equal(t, expected, actual, "ReadMsgGovManageFeesRequestFromProposalFlag result") + }) + } +} + +func TestReadFlagUint32OrDefault(t *testing.T) { + tests := []struct { + testName string + flags []string + name string // defaults to flagUint32. + def uint32 + exp uint32 + expErr string + }{ + { + testName: "error getting flag", + flags: []string{"--" + flagString, "what"}, + name: flagString, + def: 3, + exp: 3, + expErr: "trying to get uint32 value of flag of type string", + }, + { + testName: "not provided, 0 default", + def: 0, + exp: 0, + }, + { + testName: "not provided, other default", + def: 18, + exp: 18, + }, + { + testName: "provided", + flags: []string{"--" + flagUint32, "43"}, + def: 100, + exp: 43, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + if len(tc.name) == 0 { + tc.name = flagUint32 + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Uint32(flagUint32, 0, "A uint32") + flagSet.String(flagString, "", "A string") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var act uint32 + testFunc := func() { + act, err = cli.ReadFlagUint32OrDefault(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagUint32OrDefault") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagUint32OrDefault error") + assert.Equal(t, tc.exp, act, "ReadFlagUint32OrDefault result") + }) + } +} + +func TestReadFlagBoolOrDefault(t *testing.T) { + tests := []struct { + testName string + flags []string + name string // defaults to flagBool. + def bool + exp bool + expErr string + }{ + { + testName: "error getting flag, true default", + flags: []string{"--" + flagString, "what"}, + name: flagString, + def: true, + exp: true, + expErr: "trying to get bool value of flag of type string", + }, + { + testName: "error getting flag, false default", + flags: []string{"--" + flagString, "what"}, + name: flagString, + def: false, + exp: false, + expErr: "trying to get bool value of flag of type string", + }, + { + testName: "not provided, false default", + def: false, + exp: false, + }, + { + testName: "not provided, true default", + def: true, + exp: true, + }, + { + testName: "provided false, true default", + flags: []string{"--" + flagBool + "=false"}, + def: true, + exp: false, + }, + { + testName: "provided false, false default", + flags: []string{"--" + flagBool + "=false"}, + def: false, + exp: false, + }, + { + testName: "provided true, true default", + flags: []string{"--" + flagBool + "=true"}, + def: true, + exp: true, + }, + { + testName: "provided true, false default", + flags: []string{"--" + flagBool + "=true"}, + def: false, + exp: true, + }, + { + testName: "provided normal, false default", + flags: []string{"--" + flagBool}, + def: false, + exp: true, + }, + { + testName: "provided normal, true default", + flags: []string{"--" + flagBool}, + def: true, + exp: true, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + if len(tc.name) == 0 { + tc.name = flagBool + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.Bool(flagBool, false, "A bool") + flagSet.String(flagString, "", "A string") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var act bool + testFunc := func() { + act, err = cli.ReadFlagBoolOrDefault(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagBoolOrDefault") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagBoolOrDefault error") + assert.Equal(t, tc.exp, act, "ReadFlagBoolOrDefault result") + }) + } +} + +func TestReadFlagStringSliceOrDefault(t *testing.T) { + tests := []struct { + testName string + flags []string + name string // defaults to flagStringSlice. + def []string + exp []string + expErr string + }{ + { + testName: "error getting flag", + flags: []string{"--" + flagInt, "4"}, + name: flagInt, + def: []string{"eight"}, + exp: []string{"eight"}, + expErr: "trying to get stringSlice value of flag of type int", + }, + { + testName: "not provided, nil default", + def: nil, + exp: nil, + }, + { + testName: "not provided, empty default", + def: []string{}, + exp: []string{}, + }, + { + testName: "not provided, other default", + def: []string{"one", "two", "three", "fourteen"}, + exp: []string{"one", "two", "three", "fourteen"}, + }, + { + testName: "provided", + flags: []string{"--" + flagStringSlice, "one", "--" + flagStringSlice, "two,three"}, + def: []string{"seven"}, + exp: []string{"one", "two", "three"}, + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + if len(tc.name) == 0 { + tc.name = flagStringSlice + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.StringSlice(flagStringSlice, nil, "Some strings") + flagSet.Int(flagInt, 0, "An int") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var act []string + testFunc := func() { + act, err = cli.ReadFlagStringSliceOrDefault(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagStringSliceOrDefault") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagStringSliceOrDefault error") + assert.Equal(t, tc.exp, act, "ReadFlagStringSliceOrDefault result") + }) + } +} + +func TestReadFlagStringOrDefault(t *testing.T) { + tests := []struct { + testName string + flags []string + name string // defaults to flagString. + def string + exp string + expErr string + }{ + { + testName: "error getting flag", + flags: []string{"--" + flagInt, "7"}, + name: flagInt, + def: "what", + exp: "what", + expErr: "trying to get string value of flag of type int", + }, + { + testName: "not provided, empty default", + def: "", + exp: "", + }, + { + testName: "not provided, other default", + def: "other", + exp: "other", + }, + { + testName: "provided", + flags: []string{"--" + flagString, "yayaya"}, + def: "thedefault", + exp: "yayaya", + }, + } + + for _, tc := range tests { + t.Run(tc.testName, func(t *testing.T) { + if len(tc.name) == 0 { + tc.name = flagString + } + + flagSet := pflag.NewFlagSet("", pflag.ContinueOnError) + flagSet.String(flagString, "", "A string") + flagSet.Int(flagInt, 0, "A uint32") + err := flagSet.Parse(tc.flags) + require.NoError(t, err, "flagSet.Parse(%q)", tc.flags) + + var act string + testFunc := func() { + act, err = cli.ReadFlagStringOrDefault(flagSet, tc.name, tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagStringOrDefault") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagStringOrDefault error") + assert.Equal(t, tc.exp, act, "ReadFlagStringOrDefault result") + }) + } +} diff --git a/x/exchange/client/cli/helpers.go b/x/exchange/client/cli/helpers.go new file mode 100644 index 0000000000..442ad96508 --- /dev/null +++ b/x/exchange/client/cli/helpers.go @@ -0,0 +1,273 @@ +package cli + +import ( + "context" + "fmt" + "strings" + + "github.com/gogo/protobuf/proto" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "google.golang.org/grpc" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/provenance-io/provenance/x/exchange" +) + +var ( + // AuthorityAddr is the governance module's account address. + // It's not converted to a string here because the global HRP probably isn't set when this is being defined. + AuthorityAddr = authtypes.NewModuleAddress(govtypes.ModuleName) + + // ExampleAddr is an example bech32 address to use in command descriptions and stuff. + ExampleAddr = "pb1g4uxzmtsd3j5zerywf047h6lta047h6lycmzwe" // = sdk.AccAddress("ExampleAddr_________") +) + +// A msgMaker is a function that makes a Msg from a client.Context, FlagSet, and set of args. +// +// R is the type of the Msg. +type msgMaker[R sdk.Msg] func(clientCtx client.Context, flagSet *pflag.FlagSet, args []string) (R, error) + +// genericTxRunE returns a cobra.Command.RunE function that gets the client.Context and FlagSet, +// then uses the provided maker to make the Msg that it then generates or broadcasts as a Tx. +// +// R is the type of the Msg. +func genericTxRunE[R sdk.Msg](maker msgMaker[R]) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + flagSet := cmd.Flags() + msg, err := maker(clientCtx, flagSet, args) + if err != nil { + return err + } + + cmd.SilenceUsage = true + return tx.GenerateOrBroadcastTxCLI(clientCtx, flagSet, msg) + } +} + +// govTxRunE returns a cobra.Command.RunE function that gets the client.Context and FlagSet, +// then uses the provided maker to make the Msg. The Msg is then put into a governance +// proposal and either generated or broadcast as a Tx. +// +// R is the type of the Msg. +func govTxRunE[R sdk.Msg](maker msgMaker[R]) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + flagSet := cmd.Flags() + msg, err := maker(clientCtx, flagSet, args) + if err != nil { + return err + } + + cmd.SilenceUsage = true + return govcli.GenerateOrBroadcastTxCLIAsGovProp(clientCtx, flagSet, msg) + } +} + +// queryReqMaker is a function that creates a query request message. +// +// R is the type of request message. +type queryReqMaker[R any] func(clientCtx client.Context, flagSet *pflag.FlagSet, args []string) (*R, error) + +// queryEndpoint is a grpc_query endpoint function. +// +// R is the type of request message. +// S is the type of response message. +type queryEndpoint[R any, S proto.Message] func(queryClient exchange.QueryClient, ctx context.Context, req *R, opts ...grpc.CallOption) (S, error) + +// genericQueryRunE returns a cobra.Command.RunE function that gets the query context and FlagSet, +// then uses the provided maker to make the query request message. A query client is created and +// that message is then given to the provided endpoint func to get the response which is then printed. +// +// R is the type of request message. +// S is the type of response message. +func genericQueryRunE[R any, S proto.Message](reqMaker queryReqMaker[R], endpoint queryEndpoint[R, S]) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + req, err := reqMaker(clientCtx, cmd.Flags(), args) + if err != nil { + return err + } + + cmd.SilenceUsage = true + queryClient := exchange.NewQueryClient(clientCtx) + res, err := endpoint(queryClient, cmd.Context(), req) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + } +} + +// AddUseArgs adds the given strings to the cmd's Use, separated by a space. +func AddUseArgs(cmd *cobra.Command, args ...string) { + cmd.Use = cmd.Use + " " + strings.Join(args, " ") +} + +// AddUseDetails appends each provided section to the Use field with an empty line between them. +func AddUseDetails(cmd *cobra.Command, sections ...string) { + if len(sections) > 0 { + cmd.Use = cmd.Use + "\n\n" + strings.Join(sections, "\n\n") + } + cmd.DisableFlagsInUseLine = true +} + +// AddQueryExample appends an example to a query command's examples. +func AddQueryExample(cmd *cobra.Command, args ...string) { + if len(cmd.Example) > 0 { + cmd.Example += "\n" + } + cmd.Example += fmt.Sprintf("%s query %s %s", version.AppName, exchange.ModuleName, cmd.Name()) + if len(args) > 0 { + cmd.Example += " " + strings.Join(args, " ") + } +} + +// SimplePerms returns a string containing all the Permission.SimpleString() values. +func SimplePerms() string { + allPerms := exchange.AllPermissions() + simple := make([]string, len(allPerms)) + for i, perm := range allPerms { + simple[i] = perm.SimpleString() + } + return strings.Join(simple, " ") +} + +// ReqSignerDesc returns a description of how the -- flag is used and sort of required. +func ReqSignerDesc(name string) string { + return fmt.Sprintf(`If --%[1]s <%[1]s> is provided, that is used as the <%[1]s>. +If no --%[1]s is provided, the --%[2]s account address is used as the <%[1]s>. +A <%[1]s> is required.`, + name, flags.FlagFrom, + ) +} + +// ReqSignerUse is the Use string for a signer flag. +func ReqSignerUse(name string) string { + return fmt.Sprintf("{--%s|--%s} <%s>", flags.FlagFrom, name, name) +} + +// ReqFlagUse returns the string "--name " if an opt is provided, or just "--name" if not. +func ReqFlagUse(name string, opt string) string { + if len(opt) > 0 { + return fmt.Sprintf("--%s <%s>", name, opt) + } + return "--" + name +} + +// OptFlagUse wraps a ReqFlagUse in [], e.g. "[--name ]". +func OptFlagUse(name string, opt string) string { + return "[" + ReqFlagUse(name, opt) + "]" +} + +// ProposalFileDesc is a description of the --proposal flag and expected file. +func ProposalFileDesc(msgType sdk.Msg) string { + return fmt.Sprintf(`The file provided with the --%[1]s flag should be a json-encoded Tx. +The Tx should have a message with a %[2]s that contains a %[3]s. +Such a message can be generated using the --generate-only flag on the tx endpoint. + +Example (with just the important bits): +{ + "body": { + "messages": [ + { + "@type": "%[2]s", + "messages": [ + { + "@type": "%[3]s", + "authority": "...", + + } + ], + } + ], + }, +} + +If other message flags are provided with --%[1]s, they will overwrite just that field. +`, + FlagProposal, sdk.MsgTypeURL(&govv1.MsgSubmitProposal{}), sdk.MsgTypeURL(msgType), msgType, + ) +} + +var ( + // UseFlagsBreak is a string to use to start a new line of flags in the Use string of a command. + UseFlagsBreak = "\n " + + // RepeatableDesc is a description of how repeatable flags/values can be provided. + RepeatableDesc = "If a flag is repeatable, multiple entries can be separated by commas\nand/or the flag can be provided multiple times." + + // ReqAdminUse is the Use string of the --admin flag. + ReqAdminUse = fmt.Sprintf("{--%s|--%s} ", flags.FlagFrom, FlagAdmin) + + // ReqAdminDesc is a description of how the --admin, --authority, and --from flags work and are sort of required. + ReqAdminDesc = fmt.Sprintf(`If --%[1]s is provided, that is used as the . +If no --%[1]s is provided, but the --%[2]s flag was, the governance module account is used as the . +Otherwise the --%[3]s account address is used as the . +An is required.`, + FlagAdmin, FlagAuthority, flags.FlagFrom, + ) + + // ReqEnableDisableUse is a use string for the --enable and --disable flags. + ReqEnableDisableUse = fmt.Sprintf("{--%s|--%s}", FlagEnable, FlagDisable) + + // ReqEnableDisableDesc is a description of the --enable and --disable flags. + ReqEnableDisableDesc = fmt.Sprintf("One of --%s or --%s must be provided, but not both.", FlagEnable, FlagDisable) + + // AccessGrantsDesc is a description of the format. + AccessGrantsDesc = fmt.Sprintf(`An has the format "
:" +In , separate each permission with a + (plus) or . (period). +An of "
:all" will have all of the permissions. + +Example : %s:settle+update + +Valid permissions entries: %s +The full Permission enum names are also valid.`, + ExampleAddr, + SimplePerms(), + ) + + // FeeRatioDesc is a description of the format. + FeeRatioDesc = `A has the format ":". +Both and have the format "". + +Example : 100nhash:1nhash` + + // AuthorityDesc is a description of the authority flag. + AuthorityDesc = fmt.Sprintf("If --%s is not provided, the governance module account is used as the .", FlagAuthority) + + // ReqAskBidUse is a use string of the --ask and --bid flags when one is required. + ReqAskBidUse = fmt.Sprintf("{--%s|--%s}", FlagAsk, FlagBid) + + // ReqAskBidDesc is a description of the --ask and --bid flags when one is required. + ReqAskBidDesc = fmt.Sprintf("One of --%s or --%s must be provided, but not both.", FlagAsk, FlagBid) + + // OptAsksBidsUse is a use string of the optional mutually exclusive --asks and --bids flags. + OptAsksBidsUse = fmt.Sprintf("[--%s|--%s]", FlagAsks, FlagBids) + + // OptAsksBidsDesc is a description of the --asks and --bids flags when they're optional. + OptAsksBidsDesc = fmt.Sprintf("At most one of --%s or --%s can be provided.", FlagAsks, FlagBids) +) diff --git a/x/exchange/client/cli/helpers_test.go b/x/exchange/client/cli/helpers_test.go new file mode 100644 index 0000000000..e25ac22c54 --- /dev/null +++ b/x/exchange/client/cli/helpers_test.go @@ -0,0 +1,355 @@ +package cli_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" +) + +func TestAddUseArgs(t *testing.T) { + tests := []struct { + name string + use string + args []string + expUse string + }{ + { + name: "new, one arg", + use: "unit-test", + args: []string{"arg1"}, + expUse: "unit-test arg1", + }, + { + name: "new, three args", + use: "testing", + args: []string{"{--yes|--no}", "--id ", "[--thing ]"}, + expUse: "testing {--yes|--no} --id [--thing ]", + }, + { + name: "already has stuff, one arg", + use: "do-thing ", + args: []string{"[--foo]"}, + expUse: "do-thing [--foo]", + }, + { + name: "already has stuff, three args", + use: "complex ", + args: []string{"--opt1 ", "[--nope]", "[--yup]"}, + expUse: "complex --opt1 [--nope] [--yup]", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: tc.use, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + testFunc := func() { + cli.AddUseArgs(cmd, tc.args...) + } + require.NotPanics(t, testFunc, "AddUseArgs") + actUse := cmd.Use + assert.Equal(t, tc.expUse, actUse, "cmd.Use string after AddUseArgs") + }) + } +} + +func TestAddUseDetails(t *testing.T) { + tests := []struct { + name string + use string + sections []string + expUse string + }{ + { + name: "no sections", + use: "some-command {|--id } [flags]", + sections: []string{}, + expUse: "some-command {|--id } [flags]", + }, + { + name: "one section", + use: "testing [flags]", + sections: []string{"Section 1, Line 1\nSection 1, Line 2\nSection 1, Line 3"}, + expUse: `testing [flags] + +Section 1, Line 1 +Section 1, Line 2 +Section 1, Line 3`, + }, + { + name: "", + use: "longer [flags]", + sections: []string{ + "Section 1, Line 1\nSection 1, Line 2\nSection 1, Line 3", + "Section 2, Line 1\nSection 2, Line 2\nSection 2, Line 3", + "Section 3, Line 1\nSection 3, Line 2\nSection 3, Line 3", + }, + expUse: `longer [flags] + +Section 1, Line 1 +Section 1, Line 2 +Section 1, Line 3 + +Section 2, Line 1 +Section 2, Line 2 +Section 2, Line 3 + +Section 3, Line 1 +Section 3, Line 2 +Section 3, Line 3`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: tc.use, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + testFunc := func() { + cli.AddUseDetails(cmd, tc.sections...) + } + require.NotPanics(t, testFunc, "AddUseDetails") + actUse := cmd.Use + assert.Equal(t, tc.expUse, actUse, "cmd.Use string after AddUseDetails") + assert.True(t, cmd.DisableFlagsInUseLine, "cmd.DisableFlagsInUseLine") + }) + } +} + +func TestAddQueryExample(t *testing.T) { + tests := []struct { + name string + use string + example string + args []string + expExample string + }{ + { + name: "first, no args", + use: "mycmd", + expExample: version.AppName + " query exchange mycmd", + }, + { + name: "first, one arg", + use: "yourcmd", + args: []string{"--dance"}, + expExample: version.AppName + " query exchange yourcmd --dance", + }, + { + name: "first, three args", + use: "theircmd", + args: []string{"party", "someaddr", "--lights=off"}, + expExample: version.AppName + " query exchange theircmd party someaddr --lights=off", + }, + { + name: "third, no args", + use: "mycmd", + example: version.AppName + " query exchange mycmd --opt1 party\n" + + version.AppName + " query exchange mycmd --opt2 sleep", + expExample: version.AppName + " query exchange mycmd --opt1 party\n" + + version.AppName + " query exchange mycmd --opt2 sleep\n" + + version.AppName + " query exchange mycmd", + }, + { + name: "third, one arg", + use: "yourcmd", + example: version.AppName + " query exchange yourcmd --opt1 party\n" + + version.AppName + " query exchange yourcmd --opt2 sleep", + args: []string{"--no-pants"}, + expExample: version.AppName + " query exchange yourcmd --opt1 party\n" + + version.AppName + " query exchange yourcmd --opt2 sleep\n" + + version.AppName + " query exchange yourcmd --no-pants", + }, + { + name: "third, three args", + use: "theircmd", + example: version.AppName + " query exchange theircmd --opt1 party\n" + + version.AppName + " query exchange theircmd --opt2 sleep", + args: []string{"--no-shirt", "--no-shoes", "--no-service"}, + expExample: version.AppName + " query exchange theircmd --opt1 party\n" + + version.AppName + " query exchange theircmd --opt2 sleep\n" + + version.AppName + " query exchange theircmd --no-shirt --no-shoes --no-service", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: tc.use, + Example: tc.example, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("the command should not have been run") + }, + } + + testFunc := func() { + cli.AddQueryExample(cmd, tc.args...) + } + require.NotPanics(t, testFunc, "AddQueryExample") + actExample := cmd.Example + assert.Equal(t, tc.expExample, actExample, "cmd.Example string after AddQueryExample") + }) + } +} + +func TestSimplePerms(t *testing.T) { + var actual string + testFunc := func() { + actual = cli.SimplePerms() + } + require.NotPanics(t, testFunc, "SimplePerms()") + for _, perm := range exchange.AllPermissions() { + t.Run(perm.String(), func(t *testing.T) { + exp := perm.SimpleString() + assert.Contains(t, actual, exp, "SimplePerms()") + }) + } +} + +func TestReqSignerDesc(t *testing.T) { + for _, name := range []string{cli.FlagBuyer, cli.FlagSeller, cli.FlagSigner, "whatever"} { + t.Run(name, func(t *testing.T) { + var actual string + testFunc := func() { + actual = cli.ReqSignerDesc(name) + } + require.NotPanics(t, testFunc, "ReqSignerDesc(%q)", name) + assert.Contains(t, actual, "--"+name, "ReqSignerDesc(%q)", name) + assert.Contains(t, actual, "<"+name+">", "ReqSignerDesc(%q)", name) + assert.NotContains(t, actual, " "+name, "ReqSignerDesc(%q)", name) + assert.Contains(t, actual, "--"+flags.FlagFrom, "ReqSignerDesc(%q)", name) + }) + } +} + +func TestReqSignerUse(t *testing.T) { + for _, name := range []string{cli.FlagBuyer, cli.FlagSeller, cli.FlagSigner, "whatever"} { + t.Run(name, func(t *testing.T) { + var actual string + testFunc := func() { + actual = cli.ReqSignerUse(name) + } + require.NotPanics(t, testFunc, "ReqSignerUse(%q)", name) + assert.Contains(t, actual, "--"+name, "ReqSignerUse(%q)", name) + assert.Contains(t, actual, "<"+name+">", "ReqSignerUse(%q)", name) + assert.Contains(t, actual, "--"+flags.FlagFrom, "ReqSignerUse(%q)", name) + }) + } +} + +func TestReqFlagUse(t *testing.T) { + tests := []struct { + name string + opt string + exp string + }{ + {name: cli.FlagMarket, opt: "market id", exp: "--market "}, + {name: cli.FlagOrder, opt: "order id", exp: "--order "}, + {name: cli.FlagPrice, opt: "price", exp: "--price "}, + {name: "whatever", opt: "stuff", exp: "--whatever "}, + {name: cli.FlagAuthority, opt: "", exp: "--authority"}, + {name: cli.FlagEnable, opt: "", exp: "--enable"}, + {name: cli.FlagDisable, opt: "", exp: "--disable"}, + {name: "dance", opt: "", exp: "--dance"}, + } + + for _, tc := range tests { + t.Run(tc.exp, func(t *testing.T) { + var actual string + testFunc := func() { + actual = cli.ReqFlagUse(tc.name, tc.opt) + } + require.NotPanics(t, testFunc, "ReqFlagUse(%q, %q)", tc.name, tc.opt) + assert.Equal(t, tc.exp, actual, "ReqFlagUse(%q, %q)", tc.name, tc.opt) + }) + } +} + +func TestOptFlagUse(t *testing.T) { + tests := []struct { + name string + opt string + exp string + }{ + {name: cli.FlagMarket, opt: "market id", exp: "[--market ]"}, + {name: cli.FlagOrder, opt: "order id", exp: "[--order ]"}, + {name: cli.FlagPrice, opt: "price", exp: "[--price ]"}, + {name: "whatever", opt: "stuff", exp: "[--whatever ]"}, + {name: cli.FlagAuthority, opt: "", exp: "[--authority]"}, + {name: cli.FlagEnable, opt: "", exp: "[--enable]"}, + {name: cli.FlagDisable, opt: "", exp: "[--disable]"}, + {name: "dance", opt: "", exp: "[--dance]"}, + } + + for _, tc := range tests { + t.Run(tc.exp, func(t *testing.T) { + var actual string + testFunc := func() { + actual = cli.OptFlagUse(tc.name, tc.opt) + } + require.NotPanics(t, testFunc, "OptFlagUse(%q, %q)", tc.name, tc.opt) + assert.Equal(t, tc.exp, actual, "OptFlagUse(%q, %q)", tc.name, tc.opt) + }) + } +} + +func TestProposalFileDesc(t *testing.T) { + msgTypes := []sdk.Msg{ + &exchange.MsgGovCreateMarketRequest{}, + &exchange.MsgGovManageFeesRequest{}, + } + + for _, msgType := range msgTypes { + t.Run(fmt.Sprintf("%T", msgType), func(t *testing.T) { + expInRes := []string{ + "--proposal", "json", "Tx", "--generate-only", + `{ + "body": { + "messages": [ + { + "@type": "` + sdk.MsgTypeURL(&govv1.MsgSubmitProposal{}) + `", + "messages": [ + { + "@type": "` + sdk.MsgTypeURL(msgType) + `", +`, + } + + var actual string + testFunc := func() { + actual = cli.ProposalFileDesc(msgType) + } + require.NotPanics(t, testFunc, "ProposalFileDesc(%T)", msgType) + + defer func() { + if t.Failed() { + t.Logf("Result:\n%s", actual) + } + }() + + for _, exp := range expInRes { + assert.Contains(t, actual, exp, "ProposalFileDesc(%T) result. Should contain:\n%s", msgType, exp) + } + }) + } +} diff --git a/x/exchange/client/cli/query.go b/x/exchange/client/cli/query.go index 3c5cb8b522..f90b433d54 100644 --- a/x/exchange/client/cli/query.go +++ b/x/exchange/client/cli/query.go @@ -1,8 +1,221 @@ package cli -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + + "github.com/provenance-io/provenance/x/exchange" +) func CmdQuery() *cobra.Command { - // TODO[1658]: Write CmdQuery() - return nil + cmd := &cobra.Command{ + Use: exchange.ModuleName, + Aliases: []string{"ex"}, + Short: "Querying commands for the exchange module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand( + CmdQueryOrderFeeCalc(), + CmdQueryGetOrder(), + CmdQueryGetOrderByExternalID(), + CmdQueryGetMarketOrders(), + CmdQueryGetOwnerOrders(), + CmdQueryGetAssetOrders(), + CmdQueryGetAllOrders(), + CmdQueryGetMarket(), + CmdQueryGetAllMarkets(), + CmdQueryParams(), + CmdQueryValidateCreateMarket(), + CmdQueryValidateMarket(), + CmdQueryValidateManageFees(), + ) + + return cmd +} + +// CmdQueryOrderFeeCalc creates the order-fee-calc sub-command for the exchange query command. +func CmdQueryOrderFeeCalc() *cobra.Command { + cmd := &cobra.Command{ + Use: "order-fee-calc", + Aliases: []string{"fee-calc", "order-calc"}, + Short: "Calculate the fees for an order", + RunE: genericQueryRunE(MakeQueryOrderFeeCalc, exchange.QueryClient.OrderFeeCalc), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryOrderFeeCalc(cmd) + return cmd +} + +// CmdQueryGetOrder creates the order sub-command for the exchange query command. +func CmdQueryGetOrder() *cobra.Command { + cmd := &cobra.Command{ + Use: "order", + Aliases: []string{"get-order"}, + Short: "Get an order by id", + RunE: genericQueryRunE(MakeQueryGetOrder, exchange.QueryClient.GetOrder), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetOrder(cmd) + return cmd +} + +// CmdQueryGetOrderByExternalID creates the order-by-external-id sub-command for the exchange query command. +func CmdQueryGetOrderByExternalID() *cobra.Command { + cmd := &cobra.Command{ + Use: "order-by-external-id", + Aliases: []string{"get-order-by-external-id", "by-external-id", "external-id"}, + Short: "Get an order by market id and external id", + RunE: genericQueryRunE(MakeQueryGetOrderByExternalID, exchange.QueryClient.GetOrderByExternalID), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetOrderByExternalID(cmd) + return cmd +} + +// CmdQueryGetMarketOrders creates the market-orders sub-command for the exchange query command. +func CmdQueryGetMarketOrders() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-orders", + Aliases: []string{"get-market-orders"}, + Short: "Look up orders for a market", + RunE: genericQueryRunE(MakeQueryGetMarketOrders, exchange.QueryClient.GetMarketOrders), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetMarketOrders(cmd) + return cmd +} + +// CmdQueryGetOwnerOrders creates the owner-orders sub-command for the exchange query command. +func CmdQueryGetOwnerOrders() *cobra.Command { + cmd := &cobra.Command{ + Use: "owner-orders", + Aliases: []string{"get-owner-orders"}, + Short: "Look up orders with a specific owner", + RunE: genericQueryRunE(MakeQueryGetOwnerOrders, exchange.QueryClient.GetOwnerOrders), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetOwnerOrders(cmd) + return cmd +} + +// CmdQueryGetAssetOrders creates the asset-orders sub-command for the exchange query command. +func CmdQueryGetAssetOrders() *cobra.Command { + cmd := &cobra.Command{ + Use: "asset-orders", + Aliases: []string{"get-asset-orders", "denom-orders", "get-denom-orders", "assets-orders", "get-assets-orders"}, + Short: "Look up orders with a specific asset denom", + RunE: genericQueryRunE(MakeQueryGetAssetOrders, exchange.QueryClient.GetAssetOrders), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetAssetOrders(cmd) + return cmd +} + +// CmdQueryGetAllOrders creates the all-orders sub-command for the exchange query command. +func CmdQueryGetAllOrders() *cobra.Command { + cmd := &cobra.Command{ + Use: "all-orders", + Aliases: []string{"get-all-orders"}, + Short: "Get all orders", + RunE: genericQueryRunE(MakeQueryGetAllOrders, exchange.QueryClient.GetAllOrders), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetAllOrders(cmd) + return cmd +} + +// CmdQueryGetMarket creates the market sub-command for the exchange query command. +func CmdQueryGetMarket() *cobra.Command { + cmd := &cobra.Command{ + Use: "market", + Aliases: []string{"get-market"}, + Short: "Get market setup and information", + RunE: genericQueryRunE(MakeQueryGetMarket, exchange.QueryClient.GetMarket), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetMarket(cmd) + return cmd +} + +// CmdQueryGetAllMarkets creates the all-markets sub-command for the exchange query command. +func CmdQueryGetAllMarkets() *cobra.Command { + cmd := &cobra.Command{ + Use: "all-markets", + Aliases: []string{"get-all-markets"}, + Short: "Get all markets", + RunE: genericQueryRunE(MakeQueryGetAllMarkets, exchange.QueryClient.GetAllMarkets), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryGetAllMarkets(cmd) + return cmd +} + +// CmdQueryParams creates the params sub-command for the exchange query command. +func CmdQueryParams() *cobra.Command { + cmd := &cobra.Command{ + Use: "params", + Aliases: []string{"get-params"}, + Short: "Get the exchange module params", + RunE: genericQueryRunE(MakeQueryParams, exchange.QueryClient.Params), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryParams(cmd) + return cmd +} + +// CmdQueryValidateCreateMarket creates the validate-create-market sub-command for the exchange query command. +func CmdQueryValidateCreateMarket() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate-create-market", + Aliases: []string{"create-market-validate"}, + Short: "Validate a create market request", + RunE: genericQueryRunE(MakeQueryValidateCreateMarket, exchange.QueryClient.ValidateCreateMarket), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryValidateCreateMarket(cmd) + return cmd +} + +// CmdQueryValidateMarket creates the validate-market sub-command for the exchange query command. +func CmdQueryValidateMarket() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate-market", + Aliases: []string{"market-validate"}, + Short: "Validate an existing market's setup", + RunE: genericQueryRunE(MakeQueryValidateMarket, exchange.QueryClient.ValidateMarket), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryValidateMarket(cmd) + return cmd +} + +// CmdQueryValidateManageFees creates the validate-manage-fees sub-command for the exchange query command. +func CmdQueryValidateManageFees() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate-manage-fees", + Aliases: []string{"manage-fees-validate"}, + Short: "Validate a manage fees request", + RunE: genericQueryRunE(MakeQueryValidateManageFees, exchange.QueryClient.ValidateManageFees), + } + + flags.AddQueryFlagsToCmd(cmd) + SetupCmdQueryValidateManageFees(cmd) + return cmd } diff --git a/x/exchange/client/cli/query_setup.go b/x/exchange/client/cli/query_setup.go new file mode 100644 index 0000000000..f35e337be7 --- /dev/null +++ b/x/exchange/client/cli/query_setup.go @@ -0,0 +1,409 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/x/exchange" +) + +// SetupCmdQueryOrderFeeCalc adds all the flags needed for MakeQueryOrderFeeCalc. +func SetupCmdQueryOrderFeeCalc(cmd *cobra.Command) { + cmd.Flags().Bool(FlagAsk, false, "Run calculation on an ask order") + cmd.Flags().Bool(FlagBid, false, "Run calculation on a bid order") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagSeller, "", "The seller (for an ask order)") + cmd.Flags().String(FlagBuyer, "", "The buyer (for a bid order)") + cmd.Flags().String(FlagAssets, "", "The order assets") + cmd.Flags().String(FlagPrice, "", "The order price (required)") + cmd.Flags().String(FlagSettlementFee, "", "The settlement fees") + cmd.Flags().Bool(FlagPartial, false, "Allow the order to be partially filled") + cmd.Flags().String(FlagExternalID, "", "The external id") + + cmd.MarkFlagsMutuallyExclusive(FlagAsk, FlagBid) + cmd.MarkFlagsOneRequired(FlagAsk, FlagBid) + cmd.MarkFlagsMutuallyExclusive(FlagBuyer, FlagSeller) + cmd.MarkFlagsMutuallyExclusive(FlagAsk, FlagBuyer) + cmd.MarkFlagsMutuallyExclusive(FlagBid, FlagSeller) + MarkFlagsRequired(cmd, FlagMarket, FlagPrice) + + AddUseArgs(cmd, + ReqAskBidUse, + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagPrice, "price"), + ) + AddUseDetails(cmd, ReqAskBidDesc) + AddQueryExample(cmd, "--"+FlagAsk, "--"+FlagMarket, "3", "--"+FlagPrice, "10nhash") + AddQueryExample(cmd, "--"+FlagBid, "--"+FlagMarket, "3", "--"+FlagPrice, "10nhash") + + cmd.Args = cobra.NoArgs +} + +// MakeQueryOrderFeeCalc reads all the SetupCmdQueryOrderFeeCalc flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryOrderFeeCalc(_ client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.QueryOrderFeeCalcRequest, error) { + bidOrder := &exchange.BidOrder{} + + errs := make([]error, 10, 11) + var isAsk, isBid bool + isAsk, errs[0] = flagSet.GetBool(FlagAsk) + isBid, errs[1] = flagSet.GetBool(FlagBid) + bidOrder.MarketId, errs[2] = flagSet.GetUint32(FlagMarket) + var seller string + seller, errs[3] = flagSet.GetString(FlagSeller) + bidOrder.Buyer, errs[4] = flagSet.GetString(FlagBuyer) + var assets *sdk.Coin + assets, errs[5] = ReadCoinFlag(flagSet, FlagAssets) + if assets == nil { + assets = &sdk.Coin{Denom: "filler", Amount: sdkmath.NewInt(0)} + } + bidOrder.Assets = *assets + bidOrder.Price, errs[6] = ReadReqCoinFlag(flagSet, FlagPrice) + bidOrder.BuyerSettlementFees, errs[7] = ReadCoinsFlag(flagSet, FlagSettlementFee) + bidOrder.AllowPartial, errs[8] = flagSet.GetBool(FlagPartial) + bidOrder.ExternalId, errs[9] = flagSet.GetString(FlagExternalID) + + req := &exchange.QueryOrderFeeCalcRequest{} + + if isAsk { + req.AskOrder = &exchange.AskOrder{ + MarketId: bidOrder.MarketId, + Seller: seller, + Assets: bidOrder.Assets, + Price: bidOrder.Price, + AllowPartial: bidOrder.AllowPartial, + ExternalId: bidOrder.ExternalId, + } + if len(bidOrder.BuyerSettlementFees) > 0 { + req.AskOrder.SellerSettlementFlatFee = &bidOrder.BuyerSettlementFees[0] + } + if len(bidOrder.BuyerSettlementFees) > 1 { + errs = append(errs, errors.New("only one settlement fee coin is allowed for ask orders")) + } + } + + if isBid { + req.BidOrder = bidOrder + } + + return req, errors.Join(errs...) +} + +// SetupCmdQueryGetOrder adds all the flags needed for MakeQueryGetOrder. +func SetupCmdQueryGetOrder(cmd *cobra.Command) { + cmd.Flags().Uint64(FlagOrder, 0, "The order id") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagOrder), + ) + AddUseDetails(cmd, "An is required as either an arg or flag, but not both.") + AddQueryExample(cmd, "8") + AddQueryExample(cmd, "--"+FlagOrder, "8") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryGetOrder reads all the SetupCmdQueryGetOrder flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetOrder(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryGetOrderRequest, error) { + req := &exchange.QueryGetOrderRequest{} + + var err error + req.OrderId, err = ReadFlagOrderOrArg(flagSet, args) + + return req, err +} + +// SetupCmdQueryGetOrderByExternalID adds all the flags needed for MakeQueryGetOrderByExternalID. +func SetupCmdQueryGetOrderByExternalID(cmd *cobra.Command) { + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagExternalID, "", "The external id (required)") + + MarkFlagsRequired(cmd, FlagMarket, FlagExternalID) + + AddUseArgs(cmd, + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagExternalID, "external id"), + ) + AddUseDetails(cmd) + AddQueryExample(cmd, "--"+FlagMarket, "3", "--"+FlagExternalID, "12BD2C9C-9641-4370-A503-802CD7079CAA") + + cmd.Args = cobra.NoArgs +} + +// MakeQueryGetOrderByExternalID reads all the SetupCmdQueryGetOrderByExternalID flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetOrderByExternalID(_ client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.QueryGetOrderByExternalIDRequest, error) { + req := &exchange.QueryGetOrderByExternalIDRequest{} + + errs := make([]error, 2) + req.MarketId, errs[0] = flagSet.GetUint32(FlagMarket) + req.ExternalId, errs[1] = flagSet.GetString(FlagExternalID) + + return req, errors.Join(errs...) +} + +// SetupCmdQueryGetMarketOrders adds all the flags needed for MakeQueryGetMarketOrders. +func SetupCmdQueryGetMarketOrders(cmd *cobra.Command) { + flags.AddPaginationFlagsToCmd(cmd, "orders") + + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + AddFlagsAsksBidsBools(cmd) + cmd.Flags().Uint64(FlagAfter, 0, "Limit results to only orders with ids larger than this") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagMarket), + OptAsksBidsUse, + OptFlagUse(FlagAfter, "after order id"), + "[pagination flags]", + ) + AddUseDetails(cmd, + "A is required as either an arg or flag, but not both.", + OptAsksBidsDesc, + ) + AddQueryExample(cmd, "3", "--"+FlagAsks) + AddQueryExample(cmd, "--"+FlagMarket, "1", "--"+FlagAfter, "15", "--"+flags.FlagLimit, "10") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryGetMarketOrders reads all the SetupCmdQueryGetMarketOrders flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetMarketOrders(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryGetMarketOrdersRequest, error) { + req := &exchange.QueryGetMarketOrdersRequest{} + + errs := make([]error, 4) + req.MarketId, errs[0] = ReadFlagMarketOrArg(flagSet, args) + req.OrderType, errs[1] = ReadFlagsAsksBidsOpt(flagSet) + req.AfterOrderId, errs[2] = flagSet.GetUint64(FlagAfter) + req.Pagination, errs[3] = client.ReadPageRequestWithPageKeyDecoded(flagSet) + + return req, errors.Join(errs...) +} + +// SetupCmdQueryGetOwnerOrders adds all the flags needed for MakeQueryGetOwnerOrders. +func SetupCmdQueryGetOwnerOrders(cmd *cobra.Command) { + flags.AddPaginationFlagsToCmd(cmd, "orders") + + cmd.Flags().String(FlagOwner, "", "The owner") + AddFlagsAsksBidsBools(cmd) + cmd.Flags().Uint64(FlagAfter, 0, "Limit results to only orders with ids larger than this") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagOwner), + OptAsksBidsUse, + OptFlagUse(FlagAfter, "after order id"), + "[pagination flags]", + ) + AddUseDetails(cmd, + "An is required as either an arg or flag, but not both.", + OptAsksBidsDesc, + ) + AddQueryExample(cmd, ExampleAddr, "--"+FlagBids) + AddQueryExample(cmd, "--"+FlagOwner, ExampleAddr, "--"+FlagAsks, "--"+FlagAfter, "15", "--"+flags.FlagLimit, "10") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryGetOwnerOrders reads all the SetupCmdQueryGetOwnerOrders flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetOwnerOrders(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryGetOwnerOrdersRequest, error) { + req := &exchange.QueryGetOwnerOrdersRequest{} + + errs := make([]error, 5) + req.Owner, errs[0] = ReadStringFlagOrArg(flagSet, args, FlagOwner, "owner") + req.OrderType, errs[1] = ReadFlagsAsksBidsOpt(flagSet) + req.AfterOrderId, errs[2] = flagSet.GetUint64(FlagAfter) + req.Pagination, errs[3] = client.ReadPageRequestWithPageKeyDecoded(flagSet) + + return req, errors.Join(errs...) +} + +// SetupCmdQueryGetAssetOrders adds all the flags needed for MakeQueryGetAssetOrders. +func SetupCmdQueryGetAssetOrders(cmd *cobra.Command) { + flags.AddPaginationFlagsToCmd(cmd, "orders") + + cmd.Flags().String(FlagDenom, "", "The asset denom") + AddFlagsAsksBidsBools(cmd) + cmd.Flags().Uint64(FlagAfter, 0, "Limit results to only orders with ids larger than this") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagDenom), + OptAsksBidsUse, + OptFlagUse(FlagAfter, "after order id"), + "[pagination flags]", + ) + AddUseDetails(cmd, + "An is required as either an arg or flag, but not both.", + OptAsksBidsDesc, + ) + AddQueryExample(cmd, "nhash", "--"+FlagAsks) + AddQueryExample(cmd, "--"+FlagDenom, "nhash", "--"+FlagAfter, "15", "--"+flags.FlagLimit, "10") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryGetAssetOrders reads all the SetupCmdQueryGetAssetOrders flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetAssetOrders(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryGetAssetOrdersRequest, error) { + req := &exchange.QueryGetAssetOrdersRequest{} + + errs := make([]error, 4) + req.Asset, errs[0] = ReadStringFlagOrArg(flagSet, args, FlagDenom, "asset") + req.OrderType, errs[1] = ReadFlagsAsksBidsOpt(flagSet) + req.AfterOrderId, errs[2] = flagSet.GetUint64(FlagAfter) + req.Pagination, errs[3] = client.ReadPageRequestWithPageKeyDecoded(flagSet) + + return req, errors.Join(errs...) +} + +// SetupCmdQueryGetAllOrders adds all the flags needed for MakeQueryGetAllOrders. +func SetupCmdQueryGetAllOrders(cmd *cobra.Command) { + flags.AddPaginationFlagsToCmd(cmd, "orders") + + AddUseArgs(cmd, "[pagination flags]") + AddUseDetails(cmd) + AddQueryExample(cmd, "--"+flags.FlagLimit, "10") + AddQueryExample(cmd, "--"+flags.FlagReverse) + + cmd.Args = cobra.NoArgs +} + +// MakeQueryGetAllOrders reads all the SetupCmdQueryGetAllOrders flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetAllOrders(_ client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.QueryGetAllOrdersRequest, error) { + req := &exchange.QueryGetAllOrdersRequest{} + + var err error + req.Pagination, err = client.ReadPageRequestWithPageKeyDecoded(flagSet) + + return req, err +} + +// SetupCmdQueryGetMarket adds all the flags needed for MakeQueryGetMarket. +func SetupCmdQueryGetMarket(cmd *cobra.Command) { + cmd.Flags().Uint32(FlagMarket, 0, "The market id") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagMarket), + ) + AddUseDetails(cmd, "A is required as either an arg or flag, but not both.") + AddQueryExample(cmd, "3") + AddQueryExample(cmd, "--"+FlagMarket, "1") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryGetMarket reads all the SetupCmdQueryGetMarket flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetMarket(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryGetMarketRequest, error) { + req := &exchange.QueryGetMarketRequest{} + + var err error + req.MarketId, err = ReadFlagMarketOrArg(flagSet, args) + + return req, err +} + +// SetupCmdQueryGetAllMarkets adds all the flags needed for MakeQueryGetAllMarkets. +func SetupCmdQueryGetAllMarkets(cmd *cobra.Command) { + flags.AddPaginationFlagsToCmd(cmd, "markets") + + AddUseArgs(cmd, "[pagination flags]") + AddUseDetails(cmd) + AddQueryExample(cmd, "--"+flags.FlagLimit, "10") + AddQueryExample(cmd, "--"+flags.FlagReverse) + + cmd.Args = cobra.NoArgs +} + +// MakeQueryGetAllMarkets reads all the SetupCmdQueryGetAllMarkets flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryGetAllMarkets(_ client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.QueryGetAllMarketsRequest, error) { + req := &exchange.QueryGetAllMarketsRequest{} + + var err error + req.Pagination, err = client.ReadPageRequestWithPageKeyDecoded(flagSet) + + return req, err +} + +// SetupCmdQueryParams adds all the flags needed for MakeQueryParams. +func SetupCmdQueryParams(cmd *cobra.Command) { + AddUseDetails(cmd) + AddQueryExample(cmd) + + cmd.Args = cobra.NoArgs +} + +// MakeQueryParams reads all the SetupCmdQueryParams flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryParams(_ client.Context, _ *pflag.FlagSet, _ []string) (*exchange.QueryParamsRequest, error) { + return &exchange.QueryParamsRequest{}, nil +} + +// SetupCmdQueryValidateCreateMarket adds all the flags needed for MakeQueryValidateCreateMarket. +func SetupCmdQueryValidateCreateMarket(cmd *cobra.Command) { + SetupCmdTxGovCreateMarket(cmd) +} + +// MakeQueryValidateCreateMarket reads all the SetupCmdQueryValidateCreateMarket flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryValidateCreateMarket(clientCtx client.Context, flags *pflag.FlagSet, args []string) (*exchange.QueryValidateCreateMarketRequest, error) { + req := &exchange.QueryValidateCreateMarketRequest{} + + var err error + req.CreateMarketRequest, err = MakeMsgGovCreateMarket(clientCtx, flags, args) + + return req, err +} + +// SetupCmdQueryValidateMarket adds all the flags needed for MakeQueryValidateMarket. +func SetupCmdQueryValidateMarket(cmd *cobra.Command) { + cmd.Flags().Uint32(FlagMarket, 0, "The market id") + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagMarket), + ) + AddUseDetails(cmd, "A is required as either an arg or flag, but not both.") + AddQueryExample(cmd, "3") + AddQueryExample(cmd, "--"+FlagMarket, "1") + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeQueryValidateMarket reads all the SetupCmdQueryValidateMarket flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryValidateMarket(_ client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.QueryValidateMarketRequest, error) { + req := &exchange.QueryValidateMarketRequest{} + + var err error + req.MarketId, err = ReadFlagMarketOrArg(flagSet, args) + + return req, err +} + +// SetupCmdQueryValidateManageFees adds all the flags needed for MakeQueryValidateManageFees. +func SetupCmdQueryValidateManageFees(cmd *cobra.Command) { + SetupCmdTxGovManageFees(cmd) +} + +// MakeQueryValidateManageFees reads all the SetupCmdQueryValidateManageFees flags and creates the desired request. +// Satisfies the queryReqMaker type. +func MakeQueryValidateManageFees(clientCtx client.Context, flags *pflag.FlagSet, args []string) (*exchange.QueryValidateManageFeesRequest, error) { + req := &exchange.QueryValidateManageFeesRequest{} + + var err error + req.ManageFeesRequest, err = MakeMsgGovManageFees(clientCtx, flags, args) + + return req, err +} diff --git a/x/exchange/client/cli/query_setup_test.go b/x/exchange/client/cli/query_setup_test.go new file mode 100644 index 0000000000..7a9ddae92b --- /dev/null +++ b/x/exchange/client/cli/query_setup_test.go @@ -0,0 +1,1271 @@ +package cli_test + +import ( + "errors" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/version" + + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" +) + +var exampleStart = version.AppName + " query exchange dummy" + +// queryMakerTestDef is the definition of a query maker func to be tested. +// +// R is the type that is returned by the maker. +type queryMakerTestDef[R any] struct { + // makerName is the name of the maker func being tested. + makerName string + // maker is the query request maker func being tested. + maker func(clientCtx client.Context, flagSet *pflag.FlagSet, args []string) (*R, error) + // setup is the command setup func that sets up a command so it has what's needed by the maker. + setup func(cmd *cobra.Command) +} + +// queryMakerTestCase is a test case for a query maker func. +// +// R is the type that is returned by the maker. +type queryMakerTestCase[R any] struct { + // name is a name for this test case. + name string + // flags are the strings the give to FlagSet before it's provided to the maker. + flags []string + // args are the strings to supply as args to the maker. + args []string + // expReq is the expected result of the maker. + expReq *R + // expErr is the expected error string. An empty string indicates the error should be nil. + expErr string +} + +// runQueryMakerTest runs a test case for a query maker func. +// +// R is the type that is returned by the maker. +func runQueryMakerTest[R any](t *testing.T, td queryMakerTestDef[R], tc queryMakerTestCase[R]) { + cmd := &cobra.Command{ + Use: "dummy", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("this dummy command should not have been executed") + }, + } + td.setup(cmd) + + err := cmd.Flags().Parse(tc.flags) + require.NoError(t, err, "cmd.Flags().Parse(%q)", tc.flags) + + clientCtx := newClientContextWithCodec() + + var req *R + testFunc := func() { + req, err = td.maker(clientCtx, cmd.Flags(), tc.args) + } + require.NotPanics(t, testFunc, td.makerName) + assertions.AssertErrorValue(t, err, tc.expErr, "%s error", td.makerName) + assert.Equal(t, tc.expReq, req, "%s request", td.makerName) +} + +func TestSetupCmdQueryOrderFeeCalc(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryOrderFeeCalc", + setup: cli.SetupCmdQueryOrderFeeCalc, + expFlags: []string{ + cli.FlagAsk, cli.FlagBid, cli.FlagMarket, + cli.FlagSeller, cli.FlagBuyer, cli.FlagAssets, cli.FlagPrice, + cli.FlagSettlementFee, cli.FlagPartial, cli.FlagExternalID, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagAsk: { + mutExc: {cli.FlagAsk + " " + cli.FlagBid, cli.FlagAsk + " " + cli.FlagBuyer}, + oneReq: {cli.FlagAsk + " " + cli.FlagBid}, + }, + cli.FlagBid: { + mutExc: {cli.FlagAsk + " " + cli.FlagBid, cli.FlagBid + " " + cli.FlagSeller}, + oneReq: {cli.FlagAsk + " " + cli.FlagBid}, + }, + cli.FlagBuyer: {mutExc: {cli.FlagBuyer + " " + cli.FlagSeller, cli.FlagAsk + " " + cli.FlagBuyer}}, + cli.FlagSeller: {mutExc: {cli.FlagBuyer + " " + cli.FlagSeller, cli.FlagBid + " " + cli.FlagSeller}}, + cli.FlagMarket: {required: {"true"}}, + cli.FlagPrice: {required: {"true"}}, + }, + expInUse: []string{ + cli.ReqAskBidUse, "--market ", "--price ", + cli.ReqAskBidDesc, + }, + expExamples: []string{ + exampleStart + " --ask --market 3 --price 10nhash", + exampleStart + " --bid --market 3 --price 10nhash", + }, + }) +} + +func TestMakeQueryOrderFeeCalc(t *testing.T) { + td := queryMakerTestDef[exchange.QueryOrderFeeCalcRequest]{ + makerName: "MakeQueryOrderFeeCalc", + maker: cli.MakeQueryOrderFeeCalc, + setup: cli.SetupCmdQueryOrderFeeCalc, + } + + fillerCoin := sdk.Coin{Denom: "filler", Amount: sdkmath.NewInt(0)} + + tests := []queryMakerTestCase[exchange.QueryOrderFeeCalcRequest]{ + { + name: "no price and bad settlement fees", + flags: []string{"--market", "3", "--bid", "--settlement-fee", "oops"}, + expReq: &exchange.QueryOrderFeeCalcRequest{ + BidOrder: &exchange.BidOrder{ + MarketId: 3, + Assets: fillerCoin, + }, + }, + expErr: joinErrs( + "missing required --price flag", + "error parsing --settlement-fee as coins: invalid coin expression: \"oops\"", + ), + }, + { + name: "ask with two settlement fees", + flags: []string{"--market", "2", "--ask", "--settlement-fee", "10apple,3banana", "--price", "18pear"}, + expReq: &exchange.QueryOrderFeeCalcRequest{ + AskOrder: &exchange.AskOrder{ + MarketId: 2, + Assets: fillerCoin, + Price: sdk.NewInt64Coin("pear", 18), + SellerSettlementFlatFee: &sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(10)}, + }, + }, + expErr: "only one settlement fee coin is allowed for ask orders", + }, + { + name: "bad coins", + flags: []string{"--market", "11", "--price", "-3badcoin", "--assets", "noamt", "--settlement-fee", "88x"}, + expReq: &exchange.QueryOrderFeeCalcRequest{}, + expErr: joinErrs( + "error parsing --assets as a coin: invalid coin expression: \"noamt\"", + "error parsing --price as a coin: invalid coin expression: \"-3badcoin\"", + "error parsing --settlement-fee as coins: invalid coin expression: \"88x\""), + }, + { + name: "minimal ask", + flags: []string{"--ask", "--market", "51", "--price", "66prune"}, + expReq: &exchange.QueryOrderFeeCalcRequest{ + AskOrder: &exchange.AskOrder{ + MarketId: 51, + Assets: fillerCoin, + Price: sdk.NewInt64Coin("prune", 66), + }, + }, + }, + { + name: "full ask", + flags: []string{ + "--ask", "--seller", "someaddr", + "--assets", "15apple", "--price", "60plum", "--market", "8", + "--partial", "--external-id", "outsideid", + "--settlement-fee", "5fig", + }, + expReq: &exchange.QueryOrderFeeCalcRequest{ + AskOrder: &exchange.AskOrder{ + MarketId: 8, + Seller: "someaddr", + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 60), + SellerSettlementFlatFee: &sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(5)}, + AllowPartial: true, + ExternalId: "outsideid", + }, + }, + }, + { + name: "minimal bid", + flags: []string{"--bid", "--market", "51", "--price", "66prune"}, + expReq: &exchange.QueryOrderFeeCalcRequest{ + BidOrder: &exchange.BidOrder{ + MarketId: 51, + Assets: fillerCoin, + Price: sdk.NewInt64Coin("prune", 66), + }, + }, + }, + { + name: "full bid", + flags: []string{ + "--bid", "--buyer", "someaddr", + "--assets", "15apple", "--price", "60plum", "--market", "8", + "--partial", "--external-id", "outsideid", + "--settlement-fee", "5fig", + }, + expReq: &exchange.QueryOrderFeeCalcRequest{ + BidOrder: &exchange.BidOrder{ + MarketId: 8, + Buyer: "someaddr", + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 60), + BuyerSettlementFees: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(5)}}, + AllowPartial: true, + ExternalId: "outsideid", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetOrder(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetOrder", + setup: cli.SetupCmdQueryGetOrder, + expFlags: []string{cli.FlagOrder}, + expInUse: []string{ + "{|--order }", + "An is required as either an arg or flag, but not both.", + }, + expExamples: []string{ + exampleStart + " 8", + exampleStart + " --order 8", + }, + }) +} + +func TestMakeQueryGetOrder(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetOrderRequest]{ + makerName: "MakeQueryGetOrder", + maker: cli.MakeQueryGetOrder, + setup: cli.SetupCmdQueryGetOrder, + } + + tests := []queryMakerTestCase[exchange.QueryGetOrderRequest]{ + { + name: "no order id", + expReq: &exchange.QueryGetOrderRequest{}, + expErr: "no provided", + }, + { + name: "just order flag", + flags: []string{"--order", "15"}, + expReq: &exchange.QueryGetOrderRequest{OrderId: 15}, + }, + { + name: "just order id arg", + args: []string{"83"}, + expReq: &exchange.QueryGetOrderRequest{OrderId: 83}, + }, + { + name: "both order flag and arg", + flags: []string{"--order", "15"}, + args: []string{"83"}, + expReq: &exchange.QueryGetOrderRequest{}, + expErr: "cannot provide as both an arg (\"83\") and flag (--order 15)", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetOrderByExternalID(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetOrderByExternalID", + setup: cli.SetupCmdQueryGetOrderByExternalID, + expFlags: []string{ + cli.FlagMarket, cli.FlagExternalID, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagMarket: {required: {"true"}}, + cli.FlagExternalID: {required: {"true"}}, + }, + expInUse: []string{ + "--market ", "--external-id ", + }, + expExamples: []string{ + exampleStart + " --market 3 --external-id 12BD2C9C-9641-4370-A503-802CD7079CAA", + }, + }) +} + +func TestMakeQueryGetOrderByExternalID(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetOrderByExternalIDRequest]{ + makerName: "MakeQueryGetOrderByExternalID", + maker: cli.MakeQueryGetOrderByExternalID, + setup: cli.SetupCmdQueryGetOrderByExternalID, + } + + tests := []queryMakerTestCase[exchange.QueryGetOrderByExternalIDRequest]{ + { + name: "normal use", + flags: []string{"--external-id", "myid", "--market", "15"}, + expReq: &exchange.QueryGetOrderByExternalIDRequest{ + MarketId: 15, + ExternalId: "myid", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetMarketOrders(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetMarketOrders", + setup: cli.SetupCmdQueryGetMarketOrders, + expFlags: []string{ + flags.FlagPage, flags.FlagPageKey, flags.FlagOffset, + flags.FlagLimit, flags.FlagCountTotal, flags.FlagReverse, + cli.FlagMarket, cli.FlagAsks, cli.FlagBids, cli.FlagAfter, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagAsks: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + cli.FlagBids: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + }, + expInUse: []string{ + "{|--market }", cli.OptAsksBidsUse, + "[--after ", "[pagination flags]", + "A is required as either an arg or flag, but not both.", + cli.OptAsksBidsDesc, + }, + expExamples: []string{ + exampleStart + " 3 --asks", + exampleStart + " --market 1 --after 15 --limit 10", + }, + }) +} + +func TestMakeQueryGetMarketOrders(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetMarketOrdersRequest]{ + makerName: "MakeQueryGetMarketOrders", + maker: cli.MakeQueryGetMarketOrders, + setup: cli.SetupCmdQueryGetMarketOrders, + } + + defaultPageReq := &query.PageRequest{ + Key: []byte{}, + Limit: 100, + } + tests := []queryMakerTestCase[exchange.QueryGetMarketOrdersRequest]{ + { + name: "no market id", + expReq: &exchange.QueryGetMarketOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "no provided", + }, + { + name: "just market id flag", + flags: []string{"--market", "1"}, + expReq: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: defaultPageReq, + }, + }, + { + name: "just market id arg", + args: []string{"1"}, + expReq: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 1, + Pagination: defaultPageReq, + }, + }, + { + name: "both market id flag and arg", + flags: []string{"--market", "1"}, + args: []string{"1"}, + expReq: &exchange.QueryGetMarketOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "cannot provide as both an arg (\"1\") and flag (--market 1)", + }, + { + name: "all opts asks", + flags: []string{ + "--asks", "--after", "12", "--limit", "63", + "--offset", "42", "--count-total", + }, + args: []string{"7"}, + expReq: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 7, + OrderType: "ask", + AfterOrderId: 12, + Pagination: &query.PageRequest{ + Key: []byte{}, + Offset: 42, + Limit: 63, + CountTotal: true, + }, + }, + }, + { + name: "all opts bids", + flags: []string{ + "--after", "88", "--limit", "25", "--page-key", "AAAAAAAAAKA=", + "--market", "444", "--reverse", "--bids", + }, + expReq: &exchange.QueryGetMarketOrdersRequest{ + MarketId: 444, + OrderType: "bid", + AfterOrderId: 88, + Pagination: &query.PageRequest{ + Key: []byte{0, 0, 0, 0, 0, 0, 0, 160}, + Limit: 25, + Reverse: true, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetOwnerOrders(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetOwnerOrders", + setup: cli.SetupCmdQueryGetOwnerOrders, + expFlags: []string{ + flags.FlagPage, flags.FlagPageKey, flags.FlagOffset, + flags.FlagLimit, flags.FlagCountTotal, flags.FlagReverse, + cli.FlagOwner, cli.FlagAsks, cli.FlagBids, cli.FlagAfter, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagAsks: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + cli.FlagBids: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + }, + expInUse: []string{ + "{|--owner }", cli.OptAsksBidsUse, + "[--after ", "[pagination flags]", + "An is required as either an arg or flag, but not both.", + cli.OptAsksBidsDesc, + }, + expExamples: []string{ + exampleStart + " " + cli.ExampleAddr + " --bids", + exampleStart + " --owner " + cli.ExampleAddr + " --asks --after 15 --limit 10", + }, + }) +} + +func TestMakeQueryGetOwnerOrders(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetOwnerOrdersRequest]{ + makerName: "MakeQueryGetOwnerOrders", + maker: cli.MakeQueryGetOwnerOrders, + setup: cli.SetupCmdQueryGetOwnerOrders, + } + + defaultPageReq := &query.PageRequest{ + Key: []byte{}, + Limit: 100, + } + tests := []queryMakerTestCase[exchange.QueryGetOwnerOrdersRequest]{ + { + name: "no owner", + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "no provided", + }, + { + name: "just owner flag", + flags: []string{"--owner", "someaddr"}, + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Owner: "someaddr", + Pagination: defaultPageReq, + }, + }, + { + name: "just owner arg", + args: []string{"otheraddr"}, + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Owner: "otheraddr", + Pagination: defaultPageReq, + }, + }, + { + name: "both owner flag and arg", + flags: []string{"--owner", "someaddr"}, + args: []string{"otheraddr"}, + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "cannot provide as both an arg (\"otheraddr\") and flag (--owner \"someaddr\")", + }, + { + name: "all opts asks", + flags: []string{ + "--asks", "--after", "12", "--limit", "63", + "--offset", "42", "--count-total", + }, + args: []string{"otheraddr"}, + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Owner: "otheraddr", + OrderType: "ask", + AfterOrderId: 12, + Pagination: &query.PageRequest{ + Key: []byte{}, + Offset: 42, + Limit: 63, + CountTotal: true, + }, + }, + }, + { + name: "all opts bids", + flags: []string{ + "--after", "88", "--limit", "25", "--page-key", "AAAAAAAAAKA=", + "--owner", "myself", "--reverse", "--bids", + }, + expReq: &exchange.QueryGetOwnerOrdersRequest{ + Owner: "myself", + OrderType: "bid", + AfterOrderId: 88, + Pagination: &query.PageRequest{ + Key: []byte{0, 0, 0, 0, 0, 0, 0, 160}, + Limit: 25, + Reverse: true, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetAssetOrders(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetAssetOrders", + setup: cli.SetupCmdQueryGetAssetOrders, + expFlags: []string{ + flags.FlagPage, flags.FlagPageKey, flags.FlagOffset, + flags.FlagLimit, flags.FlagCountTotal, flags.FlagReverse, + cli.FlagDenom, cli.FlagAsks, cli.FlagBids, cli.FlagAfter, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagAsks: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + cli.FlagBids: {mutExc: {cli.FlagAsks + " " + cli.FlagBids}}, + }, + expInUse: []string{ + "{|--denom }", cli.OptAsksBidsUse, + "[--after ", "[pagination flags]", + "An is required as either an arg or flag, but not both.", + cli.OptAsksBidsDesc, + }, + expExamples: []string{ + exampleStart + " nhash --asks", + exampleStart + " --denom nhash --after 15 --limit 10", + }, + }) +} + +func TestMakeQueryGetAssetOrders(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetAssetOrdersRequest]{ + makerName: "MakeQueryGetAssetOrders", + maker: cli.MakeQueryGetAssetOrders, + setup: cli.SetupCmdQueryGetAssetOrders, + } + + defaultPageReq := &query.PageRequest{ + Key: []byte{}, + Limit: 100, + } + tests := []queryMakerTestCase[exchange.QueryGetAssetOrdersRequest]{ + { + name: "no denom", + expReq: &exchange.QueryGetAssetOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "no provided", + }, + { + name: "just denom flag", + flags: []string{"--denom", "mycoin"}, + expReq: &exchange.QueryGetAssetOrdersRequest{ + Asset: "mycoin", + Pagination: defaultPageReq, + }, + }, + { + name: "just denom arg", + args: []string{"yourcoin"}, + expReq: &exchange.QueryGetAssetOrdersRequest{ + Asset: "yourcoin", + Pagination: defaultPageReq, + }, + }, + { + name: "both denom flag and arg", + flags: []string{"--denom", "mycoin"}, + args: []string{"yourcoin"}, + expReq: &exchange.QueryGetAssetOrdersRequest{ + Pagination: defaultPageReq, + }, + expErr: "cannot provide as both an arg (\"yourcoin\") and flag (--denom \"mycoin\")", + }, + { + name: "all opts asks", + flags: []string{ + "--asks", "--after", "12", "--limit", "63", + "--offset", "42", "--count-total", + }, + args: []string{"yourcoin"}, + expReq: &exchange.QueryGetAssetOrdersRequest{ + Asset: "yourcoin", + OrderType: "ask", + AfterOrderId: 12, + Pagination: &query.PageRequest{ + Key: []byte{}, + Offset: 42, + Limit: 63, + CountTotal: true, + }, + }, + }, + { + name: "all opts bids", + flags: []string{ + "--after", "88", "--limit", "25", "--page-key", "AAAAAAAAAKA=", + "--denom", "mycoin", "--reverse", "--bids", + }, + expReq: &exchange.QueryGetAssetOrdersRequest{ + Asset: "mycoin", + OrderType: "bid", + AfterOrderId: 88, + Pagination: &query.PageRequest{ + Key: []byte{0, 0, 0, 0, 0, 0, 0, 160}, + Limit: 25, + Reverse: true, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetAllOrders(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetAllOrders", + setup: cli.SetupCmdQueryGetAllOrders, + expFlags: []string{ + flags.FlagPage, flags.FlagPageKey, flags.FlagOffset, + flags.FlagLimit, flags.FlagCountTotal, flags.FlagReverse, + }, + expInUse: []string{"[pagination flags]"}, + expExamples: []string{ + exampleStart + " --limit 10", + exampleStart + " --reverse", + }, + }) +} + +func TestMakeQueryGetAllOrders(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetAllOrdersRequest]{ + makerName: "MakeQueryGetAllOrders", + maker: cli.MakeQueryGetAllOrders, + setup: cli.SetupCmdQueryGetAllOrders, + } + + tests := []queryMakerTestCase[exchange.QueryGetAllOrdersRequest]{ + { + name: "no flags", + expReq: &exchange.QueryGetAllOrdersRequest{ + Pagination: &query.PageRequest{ + Key: []byte{}, + Limit: 100, + }, + }, + }, + { + name: "some pagination flags", + flags: []string{"--limit", "5", "--reverse", "--page-key", "AAAAAAAAAKA="}, + expReq: &exchange.QueryGetAllOrdersRequest{ + Pagination: &query.PageRequest{ + Key: []byte{0, 0, 0, 0, 0, 0, 0, 160}, + Limit: 5, + Reverse: true, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetMarket(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetMarket", + setup: cli.SetupCmdQueryGetMarket, + expFlags: []string{cli.FlagMarket}, + expInUse: []string{ + "{|--market }", + "A is required as either an arg or flag, but not both.", + }, + expExamples: []string{ + exampleStart + " 3", + exampleStart + " --market 1", + }, + }) +} + +func TestMakeQueryGetMarket(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetMarketRequest]{ + makerName: "MakeQueryGetMarket", + maker: cli.MakeQueryGetMarket, + setup: cli.SetupCmdQueryGetMarket, + } + + tests := []queryMakerTestCase[exchange.QueryGetMarketRequest]{ + { + name: "no market", + expReq: &exchange.QueryGetMarketRequest{}, + expErr: "no provided", + }, + { + name: "just flag", + flags: []string{"--market", "2"}, + expReq: &exchange.QueryGetMarketRequest{MarketId: 2}, + }, + { + name: "just arg", + args: []string{"1000"}, + expReq: &exchange.QueryGetMarketRequest{MarketId: 1000}, + }, + { + name: "both arg and flag", + flags: []string{"--market", "2"}, + args: []string{"1000"}, + expReq: &exchange.QueryGetMarketRequest{}, + expErr: "cannot provide as both an arg (\"1000\") and flag (--market 2)", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryGetAllMarkets(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryGetAllMarkets", + setup: cli.SetupCmdQueryGetAllMarkets, + expFlags: []string{ + flags.FlagPage, flags.FlagPageKey, flags.FlagOffset, + flags.FlagLimit, flags.FlagCountTotal, flags.FlagReverse, + }, + expInUse: []string{"[pagination flags]"}, + expExamples: []string{ + exampleStart + " --limit 10", + exampleStart + " --reverse", + }, + }) +} + +func TestMakeQueryGetAllMarkets(t *testing.T) { + td := queryMakerTestDef[exchange.QueryGetAllMarketsRequest]{ + makerName: "MakeQueryGetAllMarkets", + maker: cli.MakeQueryGetAllMarkets, + setup: cli.SetupCmdQueryGetAllMarkets, + } + + tests := []queryMakerTestCase[exchange.QueryGetAllMarketsRequest]{ + { + name: "no flags", + expReq: &exchange.QueryGetAllMarketsRequest{ + Pagination: &query.PageRequest{ + Key: []byte{}, + Limit: 100, + }, + }, + }, + { + name: "some pagination flags", + flags: []string{"--limit", "5", "--reverse", "--page-key", "AAAAAAAAAKA="}, + expReq: &exchange.QueryGetAllMarketsRequest{ + Pagination: &query.PageRequest{ + Key: []byte{0, 0, 0, 0, 0, 0, 0, 160}, + Limit: 5, + Reverse: true, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryParams(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryParams", + setup: cli.SetupCmdQueryParams, + expExamples: []string{exampleStart}, + }) +} + +func TestMakeQueryParams(t *testing.T) { + td := queryMakerTestDef[exchange.QueryParamsRequest]{ + makerName: "MakeQueryParams", + maker: cli.MakeQueryParams, + setup: cli.SetupCmdQueryParams, + } + + tests := []queryMakerTestCase[exchange.QueryParamsRequest]{ + { + name: "normal", + expReq: &exchange.QueryParamsRequest{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryValidateCreateMarket(t *testing.T) { + tc := setupTestCase{ + name: "SetupCmdQueryValidateCreateMarket", + setup: cli.SetupCmdQueryValidateCreateMarket, + expFlags: []string{ + cli.FlagAuthority, + cli.FlagMarket, cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon, + cli.FlagCreateAsk, cli.FlagCreateBid, + cli.FlagSellerFlat, cli.FlagSellerRatios, cli.FlagBuyerFlat, cli.FlagBuyerRatios, + cli.FlagAcceptingOrders, cli.FlagAllowUserSettle, cli.FlagAccessGrants, + cli.FlagReqAttrAsk, cli.FlagReqAttrBid, + cli.FlagProposal, + }, + expInUse: []string{ + "[--authority ]", "[--market ]", + "[--name ]", "[--description ]", "[--url ]", "[--icon ]", + "[--create-ask ]", "[--create-bid ]", + "[--seller-flat ]", "[--seller-ratios ]", + "[--buyer-flat ]", "[--buyer-ratios ]", + "[--accepting-orders]", "[--allow-user-settle]", + "[--access-grants ]", + "[--req-attr-ask ]", "[--req-attr-bid ]", + "[--proposal ", + cli.AuthorityDesc, cli.RepeatableDesc, cli.AccessGrantsDesc, cli.FeeRatioDesc, + cli.ProposalFileDesc(&exchange.MsgGovCreateMarketRequest{}), + }, + } + + oneReqFlags := []string{ + cli.FlagMarket, cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon, + cli.FlagCreateAsk, cli.FlagCreateBid, + cli.FlagSellerFlat, cli.FlagSellerRatios, cli.FlagBuyerFlat, cli.FlagBuyerRatios, + cli.FlagAcceptingOrders, cli.FlagAllowUserSettle, cli.FlagAccessGrants, + cli.FlagReqAttrAsk, cli.FlagReqAttrBid, + cli.FlagProposal, + } + oneReqVal := strings.Join(oneReqFlags, " ") + if tc.expAnnotations == nil { + tc.expAnnotations = make(map[string]map[string][]string) + } + for _, name := range oneReqFlags { + if tc.expAnnotations[name] == nil { + tc.expAnnotations[name] = make(map[string][]string) + } + tc.expAnnotations[name][oneReq] = []string{oneReqVal} + } + + runSetupTestCase(t, tc) +} + +func TestMakeQueryValidateCreateMarket(t *testing.T) { + td := queryMakerTestDef[exchange.QueryValidateCreateMarketRequest]{ + makerName: "MakeQueryValidateCreateMarket", + maker: cli.MakeQueryValidateCreateMarket, + setup: cli.SetupCmdQueryValidateCreateMarket, + } + + tdir := t.TempDir() + propFN := filepath.Join(tdir, "manage-fees-prop.json") + fileMsg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "A Name", + Description: "A description.", + WebsiteUrl: "A URL", + IconUri: "An Icon", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("apple", 1)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 2)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 3)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grape", 110), Fee: sdk.NewInt64Coin("grape", 10)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("date", 4)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("kiwi", 111), Fee: sdk.NewInt64Coin("kiwi", 11)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("ag1_________________").String(), + Permissions: []exchange.Permission{2}, + }, + }, + ReqAttrCreateAsk: []string{"ask.create"}, + ReqAttrCreateBid: []string{"bid.create"}, + }, + } + prop := newGovProp(t, fileMsg) + tx := newTx(t, prop) + writeFileAsJson(t, propFN, tx) + + tests := []queryMakerTestCase[exchange.QueryValidateCreateMarketRequest]{ + { + name: "several errors", + flags: []string{ + "--create-ask", "nope", "--seller-ratios", "8apple", + "--access-grants", "addr8:set", "--accepting-orders", + }, + expReq: &exchange.QueryValidateCreateMarketRequest{ + CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + FeeCreateAskFlat: []sdk.Coin{}, + FeeSellerSettlementRatios: []exchange.FeeRatio{}, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{}, + }, + }, + }, + expErr: joinErrs( + "invalid coin expression: \"nope\"", + "cannot create FeeRatio from \"8apple\": expected exactly one colon", + "could not parse permissions for \"addr8\" from \"set\": invalid permission: \"set\"", + ), + }, + { + name: "all fields", + flags: []string{ + "--authority", "otherauth", "--market", "18", + "--create-ask", "10fig", "--create-bid", "5grape", + "--seller-flat", "12fig", "--seller-ratios", "100prune:1prune", + "--buyer-flat", "17fig", "--buyer-ratios", "88plum:3plum", + "--accepting-orders", "--allow-user-settle", + "--access-grants", "addr1:settle+cancel", "--access-grants", "addr2:update+permissions", + "--req-attr-ask", "seller.kyc", "--req-attr-bid", "buyer.kyc", + "--name", "Special market", "--description", "This market is special.", + "--url", "https://example.com", "--icon", "https://example.com/icon", + "--access-grants", "addr3:all", + }, + expReq: &exchange.QueryValidateCreateMarketRequest{ + CreateMarketRequest: &exchange.MsgGovCreateMarketRequest{ + Authority: "otherauth", + Market: exchange.Market{ + MarketId: 18, + MarketDetails: exchange.MarketDetails{ + Name: "Special market", + Description: "This market is special.", + WebsiteUrl: "https://example.com", + IconUri: "https://example.com/icon", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 10)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("grape", 5)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 12)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 100), Fee: sdk.NewInt64Coin("prune", 1)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 17)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("plum", 88), Fee: sdk.NewInt64Coin("plum", 3)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: "addr1", + Permissions: []exchange.Permission{exchange.Permission_settle, exchange.Permission_cancel}, + }, + { + Address: "addr2", + Permissions: []exchange.Permission{exchange.Permission_update, exchange.Permission_permissions}, + }, + { + Address: "addr3", + Permissions: exchange.AllPermissions(), + }, + }, + ReqAttrCreateAsk: []string{"seller.kyc"}, + ReqAttrCreateBid: []string{"buyer.kyc"}, + }, + }, + }, + }, + { + name: "proposal flag", + flags: []string{"--proposal", propFN}, + expReq: &exchange.QueryValidateCreateMarketRequest{ + CreateMarketRequest: fileMsg, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryValidateMarket(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdQueryValidateMarket", + setup: cli.SetupCmdQueryValidateMarket, + expFlags: []string{cli.FlagMarket}, + expInUse: []string{ + "{|--market }", + "A is required as either an arg or flag, but not both.", + }, + expExamples: []string{ + exampleStart + " 3", + exampleStart + " --market 1", + }, + }) +} + +func TestMakeQueryValidateMarket(t *testing.T) { + td := queryMakerTestDef[exchange.QueryValidateMarketRequest]{ + makerName: "MakeQueryValidateMarket", + maker: cli.MakeQueryValidateMarket, + setup: cli.SetupCmdQueryValidateMarket, + } + + tests := []queryMakerTestCase[exchange.QueryValidateMarketRequest]{ + { + name: "no market", + expReq: &exchange.QueryValidateMarketRequest{}, + expErr: "no provided", + }, + { + name: "just flag", + flags: []string{"--market", "2"}, + expReq: &exchange.QueryValidateMarketRequest{MarketId: 2}, + }, + { + name: "just arg", + args: []string{"1000"}, + expReq: &exchange.QueryValidateMarketRequest{MarketId: 1000}, + }, + { + name: "both arg and flag", + flags: []string{"--market", "2"}, + args: []string{"1000"}, + expReq: &exchange.QueryValidateMarketRequest{}, + expErr: "cannot provide as both an arg (\"1000\") and flag (--market 2)", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} + +func TestSetupCmdQueryValidateManageFees(t *testing.T) { + tc := setupTestCase{ + name: "SetupCmdQueryValidateManageFees", + setup: cli.SetupCmdQueryValidateManageFees, + expFlags: []string{ + cli.FlagAuthority, cli.FlagMarket, + cli.FlagAskAdd, cli.FlagAskRemove, cli.FlagBidAdd, cli.FlagBidRemove, + cli.FlagSellerFlatAdd, cli.FlagSellerFlatRemove, cli.FlagSellerRatiosAdd, cli.FlagSellerRatiosRemove, + cli.FlagBuyerFlatAdd, cli.FlagBuyerFlatRemove, cli.FlagBuyerRatiosAdd, cli.FlagBuyerRatiosRemove, + cli.FlagProposal, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagMarket: {required: {"true"}}, + }, + expInUse: []string{ + "--market ", "[--authority ]", + "[--ask-add ]", "[--ask-remove ]", + "[--bid-add ]", "[--bid-remove ]", + "[--seller-flat-add ]", "[--seller-flat-remove ]", + "[--seller-ratios-add ]", "[--seller-ratios-remove ]", + "[--buyer-flat-add ]", "[--buyer-flat-remove ]", + "[--buyer-ratios-add ]", "[--buyer-ratios-remove ]", + "[--proposal ", + cli.AuthorityDesc, cli.RepeatableDesc, cli.FeeRatioDesc, + cli.ProposalFileDesc(&exchange.MsgGovManageFeesRequest{}), + }, + } + + oneReqFlags := []string{ + cli.FlagAskAdd, cli.FlagAskRemove, cli.FlagBidAdd, cli.FlagBidRemove, + cli.FlagSellerFlatAdd, cli.FlagSellerFlatRemove, cli.FlagSellerRatiosAdd, cli.FlagSellerRatiosRemove, + cli.FlagBuyerFlatAdd, cli.FlagBuyerFlatRemove, cli.FlagBuyerRatiosAdd, cli.FlagBuyerRatiosRemove, + cli.FlagProposal, + } + oneReqVal := strings.Join(oneReqFlags, " ") + if tc.expAnnotations == nil { + tc.expAnnotations = make(map[string]map[string][]string) + } + for _, name := range oneReqFlags { + if tc.expAnnotations[name] == nil { + tc.expAnnotations[name] = make(map[string][]string) + } + tc.expAnnotations[name][oneReq] = []string{oneReqVal} + } + + runSetupTestCase(t, tc) +} + +func TestMakeQueryValidateManageFees(t *testing.T) { + td := queryMakerTestDef[exchange.QueryValidateManageFeesRequest]{ + makerName: "MakeQueryValidateManageFees", + maker: cli.MakeQueryValidateManageFees, + setup: cli.SetupCmdQueryValidateManageFees, + } + + tdir := t.TempDir() + propFN := filepath.Join(tdir, "manage-fees-prop.json") + fileMsg := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 101, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("apple", 5)}, + RemoveFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("acorn", 6)}, + AddFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 7)}, + RemoveFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("blueberry", 8)}, + AddFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 9)}, + RemoveFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cantaloupe", 10)}, + AddFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grape", 100), Fee: sdk.NewInt64Coin("grape", 1)}, + }, + RemoveFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grapefruit", 101), Fee: sdk.NewInt64Coin("grapefruit", 2)}, + }, + AddFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("date", 11)}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("damson", 12)}, + AddFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("kiwi", 102), Fee: sdk.NewInt64Coin("kiwi", 3)}, + }, + RemoveFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("keylime", 104), Fee: sdk.NewInt64Coin("keylime", 4)}, + }, + } + prop := newGovProp(t, fileMsg) + tx := newTx(t, prop) + writeFileAsJson(t, propFN, tx) + + tests := []queryMakerTestCase[exchange.QueryValidateManageFeesRequest]{ + { + name: "multiple errors", + flags: []string{ + "--ask-add", "15", "--buyer-flat-remove", "noamt", + }, + expReq: &exchange.QueryValidateManageFeesRequest{ + ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + AddFeeCreateAskFlat: []sdk.Coin{}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{}, + }, + }, + expErr: joinErrs( + "invalid coin expression: \"15\"", + "invalid coin expression: \"noamt\"", + ), + }, + { + name: "all fields", + flags: []string{ + "--authority", "respect", "--market", "55", + "--ask-add", "18fig", "--ask-remove", "15fig", "--ask-add", "5grape", + "--bid-add", "17fig", "--bid-remove", "14fig", + "--seller-flat-add", "55prune", "--seller-flat-remove", "54prune", + "--seller-ratios-add", "101prune:7prune", "--seller-ratios-remove", "101prune:3prune", + "--buyer-flat-add", "59prune", "--buyer-flat-remove", "57prune", + "--buyer-ratios-add", "107prune:1prune", "--buyer-ratios-remove", "43prune:2prune", + }, + expReq: &exchange.QueryValidateManageFeesRequest{ + ManageFeesRequest: &exchange.MsgGovManageFeesRequest{ + Authority: "respect", + MarketId: 55, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 18), sdk.NewInt64Coin("grape", 5)}, + RemoveFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 15)}, + AddFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 17)}, + RemoveFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 14)}, + AddFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 55)}, + RemoveFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 54)}, + AddFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 101), Fee: sdk.NewInt64Coin("prune", 7)}, + }, + RemoveFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 101), Fee: sdk.NewInt64Coin("prune", 3)}, + }, + AddFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 59)}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 57)}, + AddFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 107), Fee: sdk.NewInt64Coin("prune", 1)}, + }, + RemoveFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 43), Fee: sdk.NewInt64Coin("prune", 2)}, + }, + }, + }, + }, + { + name: "proposal flag", + flags: []string{"--proposal", propFN}, + expReq: &exchange.QueryValidateManageFeesRequest{ + ManageFeesRequest: fileMsg, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runQueryMakerTest(t, td, tc) + }) + } +} diff --git a/x/exchange/client/cli/query_test.go b/x/exchange/client/cli/query_test.go new file mode 100644 index 0000000000..a8fa23f12c --- /dev/null +++ b/x/exchange/client/cli/query_test.go @@ -0,0 +1,547 @@ +package cli_test + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (s *CmdTestSuite) TestCmdQueryOrderFeeCalc() { + tests := []queryCmdTestCase{ + { + name: "input error", + args: []string{"order-fee-calc", "--bid", "--market", "3", "--assets", "99apple"}, + expInErr: []string{"required flag(s) \"price\" not set"}, + }, + { + name: "market does not exist", + args: []string{"fee-calc", "--market", "69", "--bid", "--price", "1000peach"}, + expInErr: []string{"market 69 does not exist", "invalid request", "InvalidArgument"}, + }, + { + name: "only ask fees, ask", + args: []string{"order-calc", "--market", "3", "--ask", "--price", "1000peach"}, + expOut: `creation_fee_options: +- amount: "10" + denom: peach +settlement_flat_fee_options: +- amount: "50" + denom: peach +settlement_ratio_fee_options: +- amount: "10" + denom: peach +`, + }, + { + name: "only ask fees, bid", + args: []string{"order-calc", "--market", "3", "--bid", "--price", "1000peach"}, + expOut: `creation_fee_options: [] +settlement_flat_fee_options: [] +settlement_ratio_fee_options: [] +`, + }, + { + name: "only bid fees, ask", + args: []string{"order-calc", "--market", "5", "--ask", "--price", "1000peach"}, + expOut: `creation_fee_options: [] +settlement_flat_fee_options: [] +settlement_ratio_fee_options: [] +`, + }, + { + name: "only bid fees, bid", + args: []string{"order-calc", "--market", "5", "--bid", "--price", "1000peach", "--output", "--json"}, + expInOut: []string{ + `"creation_fee_options":[{"denom":"peach","amount":"10"}]`, + `"settlement_flat_fee_options":[{"denom":"peach","amount":"50"}]`, + `"settlement_ratio_fee_options":[{"denom":"peach","amount":"10"},{"denom":"stake","amount":"30"}]`, + }, + }, + { + name: "both fees, ask", + args: []string{"order-calc", "--market", "420", "--ask", "--price", "1000peach", "--output", "--json"}, + expInOut: []string{ + `"creation_fee_options":[{"denom":"peach","amount":"20"}]`, + `"settlement_flat_fee_options":[{"denom":"peach","amount":"100"}]`, + `"settlement_ratio_fee_options":[{"denom":"peach","amount":"14"}]`, + }, + }, + { + name: "both fees, bid", + args: []string{"order-calc", "--market", "420", "--bid", "--price", "1000peach"}, + expOut: `creation_fee_options: +- amount: "25" + denom: peach +settlement_flat_fee_options: +- amount: "105" + denom: peach +settlement_ratio_fee_options: +- amount: "20" + denom: peach +- amount: "60" + denom: stake +`, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetOrder() { + tests := []queryCmdTestCase{ + { + name: "no order id", + args: []string{"get-order"}, + expInErr: []string{"no provided"}, + }, + { + name: "order does not exist", + args: []string{"order", "1234567899"}, + expInErr: []string{"order 1234567899 not found", "invalid request", "InvalidArgument"}, + }, + { + name: "ask order", + args: []string{"order", "--order", "42"}, + expOut: `order: + ask_order: + allow_partial: true + assets: + amount: "4200" + denom: acorn + external_id: my-id-42 + market_id: 420 + price: + amount: "17640" + denom: peach + seller: ` + s.accountAddrs[2].String() + ` + seller_settlement_flat_fee: null + order_id: "42" +`, + }, + { + name: "bid order", + args: []string{"get-order", "41", "--output", "json"}, + expInOut: []string{ + `"order_id":"41"`, + `"bid_order":`, + `"market_id":420,`, + fmt.Sprintf(`"buyer":"%s"`, s.accountAddrs[1]), + `"assets":{"denom":"apple","amount":"4100"}`, + `"price":{"denom":"peach","amount":"16810"}`, + `"buyer_settlement_fees":[]`, + `"allow_partial":false`, + `"external_id":"my-id-41"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetOrderByExternalID() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"order-by-external-id", "--external-id", "my-id-15"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "order does not exist", + args: []string{"get-order-by-external-id", "--market", "3", "--external-id", "my-id-15"}, + expInErr: []string{ + "order not found in market 3 with external id \"my-id-15\"", "invalid request", "InvalidArgument", + }, + }, + { + name: "order exists", + args: []string{"external-id", "--external-id", "my-id-15", "--market", "420", "--output", "json"}, + expInOut: []string{ + `"order_id":"15"`, + `"bid_order":`, + `"market_id":420,`, + fmt.Sprintf(`"buyer":"%s"`, s.accountAddrs[5]), + `"assets":{"denom":"acorn","amount":"1500"}`, + `"price":{"denom":"peach","amount":"2250"}`, + `"buyer_settlement_fees":[]`, + `"allow_partial":false`, + `"external_id":"my-id-15"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetMarketOrders() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"market-orders", "420", "--asks", "--bids"}, + expInErr: []string{"if any flags in the group [asks bids] are set none of the others can be; [asks bids] were all set"}, + }, + { + name: "no orders", + args: []string{"get-market-orders", "420", "--after", "1234567899"}, + expOut: `orders: [] +pagination: + next_key: null + total: "0" +`, + }, + { + name: "several orders", + args: []string{"market-orders", "--asks", "--market", "420", "--after", "30", "--output", "json", "--count-total"}, + expInOut: []string{ + `"market_id":420,`, + `"order_id":"31"`, `"order_id":"34"`, `"order_id":"36"`, + `"order_id":"37"`, `"order_id":"40"`, `"order_id":"42"`, + `"order_id":"43"`, `"order_id":"46"`, `"order_id":"48"`, + `"order_id":"49"`, `"order_id":"52"`, `"order_id":"54"`, + `"order_id":"55"`, `"order_id":"58"`, `"order_id":"60"`, + `"total":"15"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetOwnerOrders() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"owner-orders", "--limit", "10"}, + expInErr: []string{"no provided"}, + }, + { + name: "no orders", + args: []string{"get-owner-orders", sdk.AccAddress("not_gonna_have_it___").String(), "--output", "json"}, + expOut: `{"orders":[],"pagination":{"next_key":null,"total":"0"}}` + "\n", + }, + { + name: "several orders", + args: []string{"owner-orders", "--owner", s.accountAddrs[9].String()}, + expInOut: []string{ + `market_id: 420`, + `order_id: "9"`, `order_id: "19"`, `order_id: "29"`, + `order_id: "39"`, `order_id: "49"`, `order_id: "59"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetAssetOrders() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"asset-orders", "--asks"}, + expInErr: []string{"no provided"}, + }, + { + name: "no orders", + args: []string{"asset-orders", "peach"}, + expOut: `orders: [] +pagination: + next_key: null + total: "0" +`, + }, + { + name: "several orders", + args: []string{"asset-orders", "--denom", "apple", "--limit", "5", "--after", "10", "--output", "json"}, + expInOut: []string{ + `"market_id":420,`, `"order_id":"11"`, `"order_id":"12"`, + `"order_id":"13"`, `"order_id":"17"`, `"order_id":"18"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetAllOrders() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"all-orders", "extraarg"}, + expInErr: []string{"unknown command \"extraarg\" for \"exchange all-orders\""}, + }, + { + name: "no orders", + // this page key is the base64 encoded max uint64 -1, aka "the next to last possible order id." + // Hopefully these unit tests don't get up that far. + args: []string{"get-all-orders", "--page-key", "//////////4=", "--output", "json"}, + expOut: `{"orders":[],"pagination":{"next_key":null,"total":"0"}}` + "\n", + }, + { + name: "some orders", + args: []string{"all-orders", "--limit", "3", "--offset", "20"}, + expInOut: []string{ + `order_id: "21"`, `order_id: "22"`, `order_id: "23"`, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetMarket() { + tests := []queryCmdTestCase{ + { + name: "no market id", + args: []string{"market"}, + expInErr: []string{"no provided"}, + }, + { + name: "market does not exist", + args: []string{"get-market", "419"}, + expInErr: []string{"market 419 not found", "invalid request", "InvalidArgument"}, + }, + { + name: "market exists", + args: []string{"market", "420"}, + expOut: `address: cosmos1dmk5hcws5xfue8rd6pl5lu6uh8jyt9fpqs0kf6 +market: + accepting_orders: true + access_grants: + - address: ` + s.addr1.String() + ` + permissions: + - PERMISSION_SETTLE + - PERMISSION_SET_IDS + - PERMISSION_CANCEL + - PERMISSION_WITHDRAW + - PERMISSION_UPDATE + - PERMISSION_PERMISSIONS + - PERMISSION_ATTRIBUTES + allow_user_settlement: true + fee_buyer_settlement_flat: + - amount: "105" + denom: peach + fee_buyer_settlement_ratios: + - fee: + amount: "1" + denom: peach + price: + amount: "50" + denom: peach + - fee: + amount: "3" + denom: stake + price: + amount: "50" + denom: peach + fee_create_ask_flat: + - amount: "20" + denom: peach + fee_create_bid_flat: + - amount: "25" + denom: peach + fee_seller_settlement_flat: + - amount: "100" + denom: peach + fee_seller_settlement_ratios: + - fee: + amount: "1" + denom: peach + price: + amount: "75" + denom: peach + market_details: + description: It's coming; you know it. It has all the fees. + icon_uri: "" + name: THE Market + website_url: "" + market_id: 420 + req_attr_create_ask: + - seller.kyc + req_attr_create_bid: + - buyer.kyc +`, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryGetAllMarkets() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"get-all-markets", "--unexpectedflag"}, + expInErr: []string{"unknown flag: --unexpectedflag"}, + }, + { + name: "get all", + args: []string{"all-markets"}, + expInOut: []string{`market_id: 3`, `market_id: 5`, `market_id: 420`, `market_id: 421`}, + }, + { + name: "no markets", + // this page key is the base64 encoded max uint32 -1, aka "the next to last possible market id." + // Hopefully these unit tests don't get up that far. + args: []string{"get-all-markets", "--page-key", "/////g==", "--output", "json"}, + expOut: `{"markets":[],"pagination":{"next_key":null,"total":"0"}}` + "\n", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryParams() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"params", "--unexpectedflag"}, + expInErr: []string{"unknown flag: --unexpectedflag"}, + }, + { + name: "as text", + args: []string{"params", "--output", "text"}, + expOut: `params: + default_split: 500 + denom_splits: [] +`, + }, + { + name: "as json", + args: []string{"get-params", "--output", "json"}, + expOut: `{"params":{"default_split":500,"denom_splits":[]}}` + "\n", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryValidateCreateMarket() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"validate-create-market", "--create-ask", "orange"}, + expInErr: []string{"invalid coin expression: \"orange\""}, + }, + { + name: "problem with proposal", + args: []string{"create-market-validate", "--market", "420", "--name", "Other Name", "--output", "json"}, + expOut: `{"error":"market id 420 account cosmos1dmk5hcws5xfue8rd6pl5lu6uh8jyt9fpqs0kf6 already exists","gov_prop_will_pass":false}` + "\n", + }, + { + name: "okay", + args: []string{"validate-create-market", + "--name", "New Market", "--create-ask", "50nhash", "--create-bid", "50nhash", + "--accepting-orders", + }, + expOut: `error: "" +gov_prop_will_pass: true +`, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryValidateMarket() { + tests := []queryCmdTestCase{ + { + name: "no market id", + args: []string{"validate-market"}, + expInErr: []string{"no provided"}, + }, + { + name: "invalid market", + args: []string{"validate-market", "--output", "json", "--market", "421"}, + expOut: `{"error":"buyer settlement fee ratios have price denom \"plum\" but there is not a seller settlement fee ratio with that price denom"}` + "\n", + }, + { + name: "valid market", + args: []string{"market-validate", "420"}, + expOut: `error: "" +`, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdQueryValidateManageFees() { + tests := []queryCmdTestCase{ + { + name: "cmd error", + args: []string{"validate-manage-fees", "--seller-flat-add", "orange", "--market", "419"}, + expInErr: []string{"invalid coin expression: \"orange\""}, + }, + { + name: "problem with proposal", + args: []string{"manage-fees-validate", "--market", "420", + "--seller-ratios-add", "123plum:5plum", + "--buyer-ratios-add", "123pear:5pear", + }, + expOut: `error: |- + seller settlement fee ratios have price denom "plum" but there are no buyer settlement fee ratios with that price denom + buyer settlement fee ratios have price denom "pear" but there is not a seller settlement fee ratio with that price denom +gov_prop_will_pass: true +`, + }, + { + name: "fixes existing problem", + args: []string{"validate-manage-fees", "--market", "421", + "--seller-ratios-add", "123plum:5plum", "--output", "json"}, + expOut: `{"error":"","gov_prop_will_pass":true}` + "\n", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runQueryCmdTestCase(tc) + }) + } +} diff --git a/x/exchange/client/cli/tx.go b/x/exchange/client/cli/tx.go index 14b42f7b8a..48416d67f3 100644 --- a/x/exchange/client/cli/tx.go +++ b/x/exchange/client/cli/tx.go @@ -1,8 +1,278 @@ package cli -import "github.com/spf13/cobra" +import ( + "strings" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + + "github.com/provenance-io/provenance/x/exchange" +) + +// CmdTx creates the tx command (and sub-commands) for the exchange module. func CmdTx() *cobra.Command { - // TODO[1658]: Write CmdTx() - return nil + cmd := &cobra.Command{ + Use: exchange.ModuleName, + Aliases: []string{"ex"}, + Short: "Transaction commands for the exchange module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand( + CmdTxCreateAsk(), + CmdTxCreateBid(), + CmdTxCancelOrder(), + CmdTxFillBids(), + CmdTxFillAsks(), + CmdTxMarketSettle(), + CmdTxMarketSetOrderExternalID(), + CmdTxMarketWithdraw(), + CmdTxMarketUpdateDetails(), + CmdTxMarketUpdateEnabled(), + CmdTxMarketUpdateUserSettle(), + CmdTxMarketManagePermissions(), + CmdTxMarketManageReqAttrs(), + CmdTxGovCreateMarket(), + CmdTxGovManageFees(), + CmdTxGovUpdateParams(), + ) + + return cmd +} + +// CmdTxCreateAsk creates the create-ask sub-command for the exchange tx command. +func CmdTxCreateAsk() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-ask", + Aliases: []string{"ask", "create-ask-order", "ask-order"}, + Short: "Create an ask order", + RunE: genericTxRunE(MakeMsgCreateAsk), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxCreateAsk(cmd) + return cmd +} + +// CmdTxCreateBid creates the create-bid sub-command for the exchange tx command. +func CmdTxCreateBid() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-bid", + Aliases: []string{"bid", "create-bid-order", "bid-order"}, + Short: "Create a bid order", + RunE: genericTxRunE(MakeMsgCreateBid), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxCreateBid(cmd) + return cmd +} + +// CmdTxCancelOrder creates the cancel-order sub-command for the exchange tx command. +func CmdTxCancelOrder() *cobra.Command { + cmd := &cobra.Command{ + Use: "cancel-order", + Aliases: []string{"cancel"}, + Short: "Cancel an order", + RunE: genericTxRunE(MakeMsgCancelOrder), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxCancelOrder(cmd) + return cmd +} + +// CmdTxFillBids creates the fill-bids sub-command for the exchange tx command. +func CmdTxFillBids() *cobra.Command { + cmd := &cobra.Command{ + Use: "fill-bids", + Short: "Fill one or more bid orders", + RunE: genericTxRunE(MakeMsgFillBids), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxFillBids(cmd) + return cmd +} + +// CmdTxFillAsks creates the fill-asks sub-command for the exchange tx command. +func CmdTxFillAsks() *cobra.Command { + cmd := &cobra.Command{ + Use: "fill-asks", + Short: "Fill one or more ask orders", + RunE: genericTxRunE(MakeMsgFillAsks), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxFillAsks(cmd) + return cmd +} + +// CmdTxMarketSettle creates the market-settle sub-command for the exchange tx command. +func CmdTxMarketSettle() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-settle", + Aliases: []string{"settle"}, + Short: "Settle some orders", + RunE: genericTxRunE(MakeMsgMarketSettle), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketSettle(cmd) + return cmd +} + +// CmdTxMarketSetOrderExternalID creates the market-set-external-id sub-command for the exchange tx command. +func CmdTxMarketSetOrderExternalID() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-set-external-id", + Aliases: []string{"market-set-order-external-id", "set-external-id", "external-id"}, + Short: "Set an order's external id", + RunE: genericTxRunE(MakeMsgMarketSetOrderExternalID), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketSetOrderExternalID(cmd) + return cmd +} + +// CmdTxMarketWithdraw creates the market-withdraw sub-command for the exchange tx command. +func CmdTxMarketWithdraw() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-withdraw", + Aliases: []string{"withdraw"}, + Short: "Withdraw funds from a market account", + RunE: genericTxRunE(MakeMsgMarketWithdraw), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketWithdraw(cmd) + return cmd +} + +// CmdTxMarketUpdateDetails creates the market-details sub-command for the exchange tx command. +func CmdTxMarketUpdateDetails() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-details", + Aliases: []string{"market-update-details", "update-market-details", "update-details"}, + Short: "Update a market's details", + RunE: genericTxRunE(MakeMsgMarketUpdateDetails), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketUpdateDetails(cmd) + return cmd +} + +// CmdTxMarketUpdateEnabled creates the market-enabled sub-command for the exchange tx command. +func CmdTxMarketUpdateEnabled() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-enabled", + Aliases: []string{"market-update-enabled", "update-market-enabled", "update-enabled"}, + Short: "Change whether a market is accepting orders", + RunE: genericTxRunE(MakeMsgMarketUpdateEnabled), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketUpdateEnabled(cmd) + return cmd +} + +// CmdTxMarketUpdateUserSettle creates the market-user-settle sub-command for the exchange tx command. +func CmdTxMarketUpdateUserSettle() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-user-settle", + Aliases: []string{"market-update-user-settle", "update-market-user-settle", "update-user-settle"}, + Short: "Change whether a market allows settlements initiated by users", + RunE: genericTxRunE(MakeMsgMarketUpdateUserSettle), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketUpdateUserSettle(cmd) + return cmd +} + +// CmdTxMarketManagePermissions creates the market-permissions sub-command for the exchange tx command. +func CmdTxMarketManagePermissions() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-permissions", + Aliases: []string{"market-manage-permissions", "manage-market-permissions", "manage-permissions", "permissions"}, + Short: "Update the account permissions for a market", + RunE: genericTxRunE(MakeMsgMarketManagePermissions), + } + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketManagePermissions(cmd) + return cmd +} + +// CmdTxMarketManageReqAttrs creates the market-req-attrs sub-command for the exchange tx command. +func CmdTxMarketManageReqAttrs() *cobra.Command { + cmd := &cobra.Command{ + Use: "market-req-attrs", + Aliases: []string{"market-manage-req-attrs", "manage-market-req-attrs", "manage-req-attrs", "req-attrs"}, + Short: "Manage the attributes required to create orders in a market", + RunE: genericTxRunE(MakeMsgMarketManageReqAttrs), + } + newAliases := make([]string, 0, len(cmd.Aliases)) + for _, alias := range cmd.Aliases { + if strings.Contains(alias, "req-attrs") { + newAliases = append(newAliases, strings.Replace(alias, "req-attrs", "required-attributes", 1)) + } + } + cmd.Aliases = append(cmd.Aliases, newAliases...) + + flags.AddTxFlagsToCmd(cmd) + SetupCmdTxMarketManageReqAttrs(cmd) + return cmd +} + +// CmdTxGovCreateMarket creates the gov-create-market sub-command for the exchange tx command. +func CmdTxGovCreateMarket() *cobra.Command { + cmd := &cobra.Command{ + Use: "gov-create-market", + Aliases: []string{"create-market"}, + Short: "Submit a governance proposal to create a market", + RunE: govTxRunE(MakeMsgGovCreateMarket), + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + SetupCmdTxGovCreateMarket(cmd) + return cmd +} + +// CmdTxGovManageFees creates the gov-manage-fees sub-command for the exchange tx command. +func CmdTxGovManageFees() *cobra.Command { + cmd := &cobra.Command{ + Use: "gov-manage-fees", + Aliases: []string{"manage-fees", "gov-update-fees", "update-fees"}, + Short: "Submit a governance proposal to change a market's fees", + RunE: govTxRunE(MakeMsgGovManageFees), + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + SetupCmdTxGovManageFees(cmd) + return cmd +} + +// CmdTxGovUpdateParams creates the gov-update-params sub-command for the exchange tx command. +func CmdTxGovUpdateParams() *cobra.Command { + cmd := &cobra.Command{ + Use: "gov-update-params", + Aliases: []string{"gov-params", "update-params", "params"}, + Short: "Submit a governance proposal to update the exchange module params", + RunE: govTxRunE(MakeMsgGovUpdateParams), + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + SetupCmdTxGovUpdateParams(cmd) + return cmd } diff --git a/x/exchange/client/cli/tx_setup.go b/x/exchange/client/cli/tx_setup.go new file mode 100644 index 0000000000..4fb7ff3b37 --- /dev/null +++ b/x/exchange/client/cli/tx_setup.go @@ -0,0 +1,743 @@ +package cli + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + + "github.com/provenance-io/provenance/x/exchange" +) + +// SetupCmdTxCreateAsk adds all the flags needed for MakeMsgCreateAsk. +func SetupCmdTxCreateAsk(cmd *cobra.Command) { + cmd.Flags().String(FlagSeller, "", "The seller (defaults to --from account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagAssets, "", "The assets for this order, e.g. 10nhash (required)") + cmd.Flags().String(FlagPrice, "", "The price for this order, e.g. 10nhash (required)") + cmd.Flags().String(FlagSettlementFee, "", "The settlement fee Coin string for this order, e.g. 10nhash") + cmd.Flags().Bool(FlagPartial, false, "Allow this order to be partially filled") + cmd.Flags().String(FlagExternalID, "", "The external id for this order") + cmd.Flags().String(FlagCreationFee, "", "The ask order creation fee, e.g. 10nhash") + + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagSeller) + MarkFlagsRequired(cmd, FlagMarket, FlagAssets, FlagPrice) + + AddUseArgs(cmd, + ReqSignerUse(FlagSeller), + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagAssets, "assets"), + ReqFlagUse(FlagPrice, "price"), + UseFlagsBreak, + OptFlagUse(FlagSettlementFee, "seller settlement flat fee"), + OptFlagUse(FlagPartial, ""), + OptFlagUse(FlagExternalID, "external id"), + OptFlagUse(FlagCreationFee, "creation fee"), + ) + AddUseDetails(cmd, ReqSignerDesc(FlagSeller)) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgCreateAsk reads all the SetupCmdTxCreateAsk flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgCreateAsk(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgCreateAskRequest, error) { + msg := &exchange.MsgCreateAskRequest{} + + errs := make([]error, 8) + msg.AskOrder.Seller, errs[0] = ReadAddrFlagOrFrom(clientCtx, flagSet, FlagSeller) + msg.AskOrder.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.AskOrder.Assets, errs[2] = ReadReqCoinFlag(flagSet, FlagAssets) + msg.AskOrder.Price, errs[3] = ReadReqCoinFlag(flagSet, FlagPrice) + msg.AskOrder.SellerSettlementFlatFee, errs[4] = ReadCoinFlag(flagSet, FlagSettlementFee) + msg.AskOrder.AllowPartial, errs[5] = flagSet.GetBool(FlagPartial) + msg.AskOrder.ExternalId, errs[6] = flagSet.GetString(FlagExternalID) + msg.OrderCreationFee, errs[7] = ReadCoinFlag(flagSet, FlagCreationFee) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxCreateBid adds all the flags needed for MakeMsgCreateBid. +func SetupCmdTxCreateBid(cmd *cobra.Command) { + cmd.Flags().String(FlagBuyer, "", "The buyer (defaults to --from account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagAssets, "", "The assets for this order, e.g. 10nhash (required)") + cmd.Flags().String(FlagPrice, "", "The price for this order, e.g. 10nhash (required)") + cmd.Flags().String(FlagSettlementFee, "", "The settlement fee Coin string for this order, e.g. 10nhash") + cmd.Flags().Bool(FlagPartial, false, "Allow this order to be partially filled") + cmd.Flags().String(FlagExternalID, "", "The external id for this order") + cmd.Flags().String(FlagCreationFee, "", "The bid order creation fee, e.g. 10nhash") + + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagBuyer) + MarkFlagsRequired(cmd, FlagMarket, FlagAssets, FlagPrice) + + AddUseArgs(cmd, + ReqSignerUse(FlagBuyer), + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagAssets, "assets"), + ReqFlagUse(FlagPrice, "price"), + UseFlagsBreak, + OptFlagUse(FlagSettlementFee, "seller settlement flat fee"), + OptFlagUse(FlagPartial, ""), + OptFlagUse(FlagExternalID, "external id"), + OptFlagUse(FlagCreationFee, "creation fee"), + ) + AddUseDetails(cmd, ReqSignerDesc(FlagBuyer)) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgCreateBid reads all the SetupCmdTxCreateBid flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgCreateBid(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgCreateBidRequest, error) { + msg := &exchange.MsgCreateBidRequest{} + + errs := make([]error, 8) + msg.BidOrder.Buyer, errs[0] = ReadAddrFlagOrFrom(clientCtx, flagSet, FlagBuyer) + msg.BidOrder.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.BidOrder.Assets, errs[2] = ReadReqCoinFlag(flagSet, FlagAssets) + msg.BidOrder.Price, errs[3] = ReadReqCoinFlag(flagSet, FlagPrice) + msg.BidOrder.BuyerSettlementFees, errs[4] = ReadCoinsFlag(flagSet, FlagSettlementFee) + msg.BidOrder.AllowPartial, errs[5] = flagSet.GetBool(FlagPartial) + msg.BidOrder.ExternalId, errs[6] = flagSet.GetString(FlagExternalID) + msg.OrderCreationFee, errs[7] = ReadCoinFlag(flagSet, FlagCreationFee) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxCancelOrder adds all the flags needed for the MakeMsgCancelOrder. +func SetupCmdTxCancelOrder(cmd *cobra.Command) { + cmd.Flags().String(FlagSigner, "", "The signer (defaults to --from account)") + cmd.Flags().Uint64(FlagOrder, 0, "The order id") + + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagSigner) + + AddUseArgs(cmd, + fmt.Sprintf("{|--%s }", FlagOrder), + ReqSignerUse(FlagSigner), + ) + AddUseDetails(cmd, + ReqSignerDesc(FlagSigner), + "The must be provided either as the first argument or using the --order flag, but not both.", + ) + + cmd.Args = cobra.MaximumNArgs(1) +} + +// MakeMsgCancelOrder reads all the SetupCmdTxCancelOrder flags and the provided args and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgCancelOrder(clientCtx client.Context, flagSet *pflag.FlagSet, args []string) (*exchange.MsgCancelOrderRequest, error) { + msg := &exchange.MsgCancelOrderRequest{} + + errs := make([]error, 2) + msg.Signer, errs[0] = ReadAddrFlagOrFrom(clientCtx, flagSet, FlagSigner) + msg.OrderId, errs[1] = ReadFlagOrderOrArg(flagSet, args) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxFillBids adds all the flags needed for MakeMsgFillBids. +func SetupCmdTxFillBids(cmd *cobra.Command) { + cmd.Flags().String(FlagSeller, "", "The seller (defaults to --from account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagAssets, "", "The total assets you are filling, e.g. 10nhash (required)") + cmd.Flags().UintSlice(FlagBids, nil, "The bid order ids (repeatable, required)") + cmd.Flags().String(FlagSettlementFee, "", "The settlement fee Coin string for this order, e.g. 10nhash") + cmd.Flags().String(FlagCreationFee, "", "The ask order creation fee, e.g. 10nhash") + + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagSeller) + MarkFlagsRequired(cmd, FlagMarket, FlagAssets, FlagBids) + + AddUseArgs(cmd, + ReqSignerUse(FlagSeller), + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagAssets, "total assets"), + ReqFlagUse(FlagBids, "bid order ids"), + UseFlagsBreak, + OptFlagUse(FlagSettlementFee, "seller settlement flat fee"), + OptFlagUse(FlagCreationFee, "ask order creation fee"), + ) + AddUseDetails(cmd, ReqSignerDesc(FlagSeller), RepeatableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgFillBids reads all the SetupCmdTxFillBids flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgFillBids(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgFillBidsRequest, error) { + msg := &exchange.MsgFillBidsRequest{} + + errs := make([]error, 6) + msg.Seller, errs[0] = ReadAddrFlagOrFrom(clientCtx, flagSet, FlagSeller) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.TotalAssets, errs[2] = ReadCoinsFlag(flagSet, FlagAssets) + msg.BidOrderIds, errs[3] = ReadOrderIDsFlag(flagSet, FlagBids) + msg.SellerSettlementFlatFee, errs[4] = ReadCoinFlag(flagSet, FlagSettlementFee) + msg.AskOrderCreationFee, errs[5] = ReadCoinFlag(flagSet, FlagCreationFee) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxFillAsks adds all the flags needed for MakeMsgFillAsks. +func SetupCmdTxFillAsks(cmd *cobra.Command) { + cmd.Flags().String(FlagBuyer, "", "The buyer (defaults to --from account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagPrice, "", "The total price you are paying, e.g. 10nhash (required)") + cmd.Flags().UintSlice(FlagAsks, nil, "The ask order ids (repeatable, required)") + cmd.Flags().String(FlagSettlementFee, "", "The settlement fee Coin string for this order, e.g. 10nhash") + cmd.Flags().String(FlagCreationFee, "", "The bid order creation fee, e.g. 10nhash") + + cmd.MarkFlagsOneRequired(flags.FlagFrom, FlagBuyer) + MarkFlagsRequired(cmd, FlagMarket, FlagPrice, FlagAsks) + + AddUseArgs(cmd, + ReqSignerUse(FlagBuyer), + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagPrice, "total price"), + ReqFlagUse(FlagAsks, "ask order ids"), + UseFlagsBreak, + OptFlagUse(FlagSettlementFee, "buyer settlement fees"), + OptFlagUse(FlagCreationFee, "bid order creation fee"), + ) + AddUseDetails(cmd, ReqSignerDesc(FlagBuyer), RepeatableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgFillAsks reads all the SetupCmdTxFillAsks flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgFillAsks(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgFillAsksRequest, error) { + msg := &exchange.MsgFillAsksRequest{} + + errs := make([]error, 6) + msg.Buyer, errs[0] = ReadAddrFlagOrFrom(clientCtx, flagSet, FlagBuyer) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.TotalPrice, errs[2] = ReadReqCoinFlag(flagSet, FlagPrice) + msg.AskOrderIds, errs[3] = ReadOrderIDsFlag(flagSet, FlagAsks) + msg.BuyerSettlementFees, errs[4] = ReadCoinsFlag(flagSet, FlagSettlementFee) + msg.BidOrderCreationFee, errs[5] = ReadCoinFlag(flagSet, FlagCreationFee) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketSettle adds all the flags needed for MakeMsgMarketSettle. +func SetupCmdTxMarketSettle(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().UintSlice(FlagAsks, nil, "The ask order ids (repeatable, required)") + cmd.Flags().UintSlice(FlagBids, nil, "The bid order ids (repeatable, required)") + cmd.Flags().Bool(FlagPartial, false, "Expect partial settlement") + + MarkFlagsRequired(cmd, FlagMarket, FlagAsks, FlagBids) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagAsks, "ask order ids"), + ReqFlagUse(FlagBids, "bid order ids"), + OptFlagUse(FlagPartial, ""), + ) + AddUseDetails(cmd, ReqAdminDesc, RepeatableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketSettle reads all the SetupCmdTxMarketSettle flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketSettle(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketSettleRequest, error) { + msg := &exchange.MsgMarketSettleRequest{} + + errs := make([]error, 5) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.AskOrderIds, errs[2] = ReadOrderIDsFlag(flagSet, FlagAsks) + msg.BidOrderIds, errs[3] = ReadOrderIDsFlag(flagSet, FlagBids) + msg.ExpectPartial, errs[4] = flagSet.GetBool(FlagPartial) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketSetOrderExternalID adds all the flags needed for MakeMsgMarketSetOrderExternalID. +func SetupCmdTxMarketSetOrderExternalID(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().Uint64(FlagOrder, 0, "The order id (required)") + cmd.Flags().String(FlagExternalID, "", "The new external id for this order") + + MarkFlagsRequired(cmd, FlagMarket, FlagOrder) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagOrder, "order id"), + OptFlagUse(FlagExternalID, "external id"), + ) + AddUseDetails(cmd, ReqAdminDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketSetOrderExternalID reads all the SetupCmdTxMarketSetOrderExternalID flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketSetOrderExternalID(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketSetOrderExternalIDRequest, error) { + msg := &exchange.MsgMarketSetOrderExternalIDRequest{} + + errs := make([]error, 4) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.OrderId, errs[2] = flagSet.GetUint64(FlagOrder) + msg.ExternalId, errs[3] = flagSet.GetString(FlagExternalID) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketWithdraw adds all the flags needed for MakeMsgMarketWithdraw. +func SetupCmdTxMarketWithdraw(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().String(FlagTo, "", "The address that will receive the funds (required)") + cmd.Flags().String(FlagAmount, "", "The amount to withdraw (required)") + + MarkFlagsRequired(cmd, FlagMarket, FlagTo, FlagAmount) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + ReqFlagUse(FlagTo, "to address"), + ReqFlagUse(FlagAmount, "amount"), + ) + AddUseDetails(cmd, ReqAdminDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketWithdraw reads all the SetupCmdTxMarketWithdraw flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketWithdraw(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketWithdrawRequest, error) { + msg := &exchange.MsgMarketWithdrawRequest{} + + errs := make([]error, 4) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.ToAddress, errs[2] = flagSet.GetString(FlagTo) + msg.Amount, errs[3] = ReadCoinsFlag(flagSet, FlagAmount) + + return msg, errors.Join(errs...) +} + +// AddFlagsMarketDetails adds all the flags needed for ReadFlagsMarketDetails. +func AddFlagsMarketDetails(cmd *cobra.Command) { + cmd.Flags().String(FlagName, "", fmt.Sprintf("A short name for the market (max %d chars)", exchange.MaxName)) + cmd.Flags().String(FlagDescription, "", fmt.Sprintf("A description of the market (max %d chars)", exchange.MaxDescription)) + cmd.Flags().String(FlagURL, "", fmt.Sprintf("The market's website URL (max %d chars)", exchange.MaxWebsiteURL)) + cmd.Flags().String(FlagIcon, "", fmt.Sprintf("The market's icon URI (max %d chars)", exchange.MaxIconURI)) +} + +// ReadFlagsMarketDetails reads all the AddFlagsMarketDetails flags and creates the desired MarketDetails. +func ReadFlagsMarketDetails(flagSet *pflag.FlagSet, def exchange.MarketDetails) (exchange.MarketDetails, error) { + rv := exchange.MarketDetails{} + + errs := make([]error, 4) + rv.Name, errs[0] = ReadFlagStringOrDefault(flagSet, FlagName, def.Name) + rv.Description, errs[1] = ReadFlagStringOrDefault(flagSet, FlagDescription, def.Description) + rv.WebsiteUrl, errs[2] = ReadFlagStringOrDefault(flagSet, FlagURL, def.WebsiteUrl) + rv.IconUri, errs[3] = ReadFlagStringOrDefault(flagSet, FlagIcon, def.IconUri) + + return rv, errors.Join(errs...) +} + +// SetupCmdTxMarketUpdateDetails adds all the flags needed for MakeMsgMarketUpdateDetails. +func SetupCmdTxMarketUpdateDetails(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + AddFlagsMarketDetails(cmd) + + MarkFlagsRequired(cmd, FlagMarket) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + UseFlagsBreak, + OptFlagUse(FlagName, "name"), + OptFlagUse(FlagDescription, "description"), + OptFlagUse(FlagURL, "website url"), + OptFlagUse(FlagIcon, "icon uri"), + ) + AddUseDetails(cmd, + ReqAdminDesc, + `All fields of a market's details will be updated. +If you omit an optional flag, that field will be updated to an empty string.`, + ) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketUpdateDetails reads all the SetupCmdTxMarketUpdateDetails flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketUpdateDetails(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketUpdateDetailsRequest, error) { + msg := &exchange.MsgMarketUpdateDetailsRequest{} + + errs := make([]error, 3) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.MarketDetails, errs[2] = ReadFlagsMarketDetails(flagSet, exchange.MarketDetails{}) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketUpdateEnabled adds all the flags needed for MakeMsgMarketUpdateEnabled. +func SetupCmdTxMarketUpdateEnabled(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + AddFlagsEnableDisable(cmd, "accepting_orders") + + MarkFlagsRequired(cmd, FlagMarket) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + ReqEnableDisableUse, + ) + AddUseDetails(cmd, ReqAdminDesc, ReqEnableDisableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketUpdateEnabled reads all the SetupCmdTxMarketUpdateEnabled flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketUpdateEnabled(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketUpdateEnabledRequest, error) { + msg := &exchange.MsgMarketUpdateEnabledRequest{} + + errs := make([]error, 3) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.AcceptingOrders, errs[2] = ReadFlagsEnableDisable(flagSet) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketUpdateUserSettle adds all the flags needed for MakeMsgMarketUpdateUserSettle. +func SetupCmdTxMarketUpdateUserSettle(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + AddFlagsEnableDisable(cmd, "allow_user_settlement") + + MarkFlagsRequired(cmd, FlagMarket) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + ReqEnableDisableUse, + ) + AddUseDetails(cmd, ReqAdminDesc, ReqEnableDisableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketUpdateUserSettle reads all the SetupCmdTxMarketUpdateUserSettle flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketUpdateUserSettle(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketUpdateUserSettleRequest, error) { + msg := &exchange.MsgMarketUpdateUserSettleRequest{} + + errs := make([]error, 3) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.AllowUserSettlement, errs[2] = ReadFlagsEnableDisable(flagSet) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketManagePermissions adds all the flags needed for MakeMsgMarketManagePermissions. +func SetupCmdTxMarketManagePermissions(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().StringSlice(FlagRevokeAll, nil, "Addresses to revoke all permissions from (repeatable)") + cmd.Flags().StringSlice(FlagRevoke, nil, " to remove from the market (repeatable)") + cmd.Flags().StringSlice(FlagGrant, nil, " to add to the market (repeatable)") + + cmd.MarkFlagsOneRequired(FlagRevokeAll, FlagRevoke, FlagGrant) + MarkFlagsRequired(cmd, FlagMarket) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + UseFlagsBreak, + OptFlagUse(FlagRevokeAll, "addresses"), + OptFlagUse(FlagRevoke, "access grants"), + OptFlagUse(FlagGrant, "access grants"), + ) + AddUseDetails(cmd, ReqAdminDesc, RepeatableDesc, AccessGrantsDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketManagePermissions reads all the SetupCmdTxMarketManagePermissions flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketManagePermissions(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketManagePermissionsRequest, error) { + msg := &exchange.MsgMarketManagePermissionsRequest{} + + errs := make([]error, 5) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.RevokeAll, errs[2] = flagSet.GetStringSlice(FlagRevokeAll) + msg.ToRevoke, errs[3] = ReadAccessGrantsFlag(flagSet, FlagRevoke, nil) + msg.ToGrant, errs[4] = ReadAccessGrantsFlag(flagSet, FlagGrant, nil) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxMarketManageReqAttrs adds all the flags needed for MakeMsgMarketManageReqAttrs. +func SetupCmdTxMarketManageReqAttrs(cmd *cobra.Command) { + AddFlagsAdmin(cmd) + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().StringSlice(FlagAskAdd, nil, "The create-ask required attributes to add (repeatable)") + cmd.Flags().StringSlice(FlagAskRemove, nil, "The create-ask required attributes to remove (repeatable)") + cmd.Flags().StringSlice(FlagBidAdd, nil, "The create-bid required attributes to add (repeatable)") + cmd.Flags().StringSlice(FlagBidRemove, nil, "The create-bid required attributes to remove (repeatable)") + + cmd.MarkFlagsOneRequired(FlagAskAdd, FlagAskRemove, FlagBidAdd, FlagBidRemove) + MarkFlagsRequired(cmd, FlagMarket) + + AddUseArgs(cmd, + ReqAdminUse, + ReqFlagUse(FlagMarket, "market id"), + UseFlagsBreak, + OptFlagUse(FlagAskAdd, "attrs"), + OptFlagUse(FlagAskRemove, "attrs"), + UseFlagsBreak, + OptFlagUse(FlagBidAdd, "attrs"), + OptFlagUse(FlagBidRemove, "attrs"), + ) + AddUseDetails(cmd, ReqAdminDesc, RepeatableDesc) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgMarketManageReqAttrs reads all the SetupCmdTxMarketManageReqAttrs flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgMarketManageReqAttrs(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgMarketManageReqAttrsRequest, error) { + msg := &exchange.MsgMarketManageReqAttrsRequest{} + + errs := make([]error, 6) + msg.Admin, errs[0] = ReadFlagsAdminOrFrom(clientCtx, flagSet) + msg.MarketId, errs[1] = flagSet.GetUint32(FlagMarket) + msg.CreateAskToAdd, errs[2] = flagSet.GetStringSlice(FlagAskAdd) + msg.CreateAskToRemove, errs[3] = flagSet.GetStringSlice(FlagAskRemove) + msg.CreateBidToAdd, errs[4] = flagSet.GetStringSlice(FlagBidAdd) + msg.CreateBidToRemove, errs[5] = flagSet.GetStringSlice(FlagBidRemove) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxGovCreateMarket adds all the flags needed for MakeMsgGovCreateMarket. +func SetupCmdTxGovCreateMarket(cmd *cobra.Command) { + cmd.Flags().String(FlagAuthority, "", "The authority address to use (defaults to the governance module account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id") + AddFlagsMarketDetails(cmd) + cmd.Flags().StringSlice(FlagCreateAsk, nil, "The create-ask fee options, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagCreateBid, nil, "The create-bid fee options, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerFlat, nil, "The seller settlement flat fee options, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerRatios, nil, "The seller settlement fee ratios, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerFlat, nil, "The buyer settlement flat fee options, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerRatios, nil, "The buyer settlement fee ratios, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().Bool(FlagAcceptingOrders, false, "The market should allow orders to be created") + cmd.Flags().Bool(FlagAllowUserSettle, false, "The market should allow user-initiated settlement") + cmd.Flags().StringSlice(FlagAccessGrants, nil, "The that the market should have (repeatable)") + cmd.Flags().StringSlice(FlagReqAttrAsk, nil, "Attributes required to create ask orders (repeatable)") + cmd.Flags().StringSlice(FlagReqAttrBid, nil, "Attributes required to create bid orders (repeatable)") + cmd.Flags().String(FlagProposal, "", "a json file of a Tx with a gov proposal with a MsgGovCreateMarketRequest") + + cmd.MarkFlagsOneRequired( + FlagMarket, FlagName, FlagDescription, FlagURL, FlagIcon, + FlagCreateAsk, FlagCreateBid, + FlagSellerFlat, FlagSellerRatios, FlagBuyerFlat, FlagBuyerRatios, + FlagAcceptingOrders, FlagAllowUserSettle, FlagAccessGrants, + FlagReqAttrAsk, FlagReqAttrBid, + FlagProposal, + ) + + AddUseArgs(cmd, + OptFlagUse(FlagAuthority, "authority"), + OptFlagUse(FlagMarket, "market id"), + UseFlagsBreak, + OptFlagUse(FlagName, "name"), + OptFlagUse(FlagDescription, "description"), + OptFlagUse(FlagURL, "website url"), + OptFlagUse(FlagIcon, "icon uri"), + UseFlagsBreak, + OptFlagUse(FlagCreateAsk, "coins"), + OptFlagUse(FlagCreateBid, "coins"), + UseFlagsBreak, + OptFlagUse(FlagSellerFlat, "coins"), + OptFlagUse(FlagSellerRatios, "fee ratios"), + UseFlagsBreak, + OptFlagUse(FlagBuyerFlat, "coins"), + OptFlagUse(FlagBuyerRatios, "fee ratios"), + UseFlagsBreak, + OptFlagUse(FlagAcceptingOrders, ""), + OptFlagUse(FlagAllowUserSettle, ""), + UseFlagsBreak, + OptFlagUse(FlagAccessGrants, "access grants"), + UseFlagsBreak, + OptFlagUse(FlagReqAttrAsk, "attrs"), + OptFlagUse(FlagReqAttrBid, "attrs"), + UseFlagsBreak, + OptFlagUse(FlagProposal, "json filename"), + ) + AddUseDetails(cmd, + AuthorityDesc, RepeatableDesc, AccessGrantsDesc, FeeRatioDesc, + ProposalFileDesc(&exchange.MsgGovCreateMarketRequest{}), + ) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgGovCreateMarket reads all the SetupCmdTxGovCreateMarket flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgGovCreateMarket(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgGovCreateMarketRequest, error) { + var msg *exchange.MsgGovCreateMarketRequest + + errs := make([]error, 15) + msg, errs[0] = ReadMsgGovCreateMarketRequestFromProposalFlag(clientCtx, flagSet) + msg.Authority, errs[1] = ReadFlagAuthorityOrDefault(flagSet, msg.Authority) + msg.Market.MarketId, errs[2] = ReadFlagUint32OrDefault(flagSet, FlagMarket, msg.Market.MarketId) + msg.Market.MarketDetails, errs[3] = ReadFlagsMarketDetails(flagSet, msg.Market.MarketDetails) + msg.Market.FeeCreateAskFlat, errs[4] = ReadFlatFeeFlag(flagSet, FlagCreateAsk, msg.Market.FeeCreateAskFlat) + msg.Market.FeeCreateBidFlat, errs[5] = ReadFlatFeeFlag(flagSet, FlagCreateBid, msg.Market.FeeCreateBidFlat) + msg.Market.FeeSellerSettlementFlat, errs[6] = ReadFlatFeeFlag(flagSet, FlagSellerFlat, msg.Market.FeeSellerSettlementFlat) + msg.Market.FeeSellerSettlementRatios, errs[7] = ReadFeeRatiosFlag(flagSet, FlagSellerRatios, msg.Market.FeeSellerSettlementRatios) + msg.Market.FeeBuyerSettlementFlat, errs[8] = ReadFlatFeeFlag(flagSet, FlagBuyerFlat, msg.Market.FeeBuyerSettlementFlat) + msg.Market.FeeBuyerSettlementRatios, errs[9] = ReadFeeRatiosFlag(flagSet, FlagBuyerRatios, msg.Market.FeeBuyerSettlementRatios) + msg.Market.AcceptingOrders, errs[10] = ReadFlagBoolOrDefault(flagSet, FlagAcceptingOrders, msg.Market.AcceptingOrders) + msg.Market.AllowUserSettlement, errs[11] = ReadFlagBoolOrDefault(flagSet, FlagAllowUserSettle, msg.Market.AllowUserSettlement) + msg.Market.AccessGrants, errs[12] = ReadAccessGrantsFlag(flagSet, FlagAccessGrants, msg.Market.AccessGrants) + msg.Market.ReqAttrCreateAsk, errs[13] = ReadFlagStringSliceOrDefault(flagSet, FlagReqAttrAsk, msg.Market.ReqAttrCreateAsk) + msg.Market.ReqAttrCreateBid, errs[14] = ReadFlagStringSliceOrDefault(flagSet, FlagReqAttrBid, msg.Market.ReqAttrCreateBid) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxGovManageFees adds all the flags needed for MakeMsgGovManageFees. +func SetupCmdTxGovManageFees(cmd *cobra.Command) { + cmd.Flags().String(FlagAuthority, "", "The authority address to use (defaults to the governance module account)") + cmd.Flags().Uint32(FlagMarket, 0, "The market id (required)") + cmd.Flags().StringSlice(FlagAskAdd, nil, "Create-ask flat fee options to add, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagAskRemove, nil, "Create-ask flat fee options to remove, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagBidAdd, nil, "Create-bid flat fee options to add, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagBidRemove, nil, "Create-bid flat fee options to remove, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerFlatAdd, nil, "Seller settlement flat fee options to add, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerFlatRemove, nil, "Seller settlement flat fee options to remove, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerRatiosAdd, nil, "Seller settlement fee ratios to add, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().StringSlice(FlagSellerRatiosRemove, nil, "Seller settlement fee ratios to remove, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerFlatAdd, nil, "Buyer settlement flat fee options to add, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerFlatRemove, nil, "Buyer settlement flat fee options to remove, e.g. 10nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerRatiosAdd, nil, "Seller settlement fee ratios to add, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().StringSlice(FlagBuyerRatiosRemove, nil, "Seller settlement fee ratios to remove, e.g. 100nhash:1nhash (repeatable)") + cmd.Flags().String(FlagProposal, "", "a json file of a Tx with a gov proposal with a MsgGovManageFeesRequest") + + MarkFlagsRequired(cmd, FlagMarket) + cmd.MarkFlagsOneRequired( + FlagAskAdd, FlagAskRemove, FlagBidAdd, FlagBidRemove, + FlagSellerFlatAdd, FlagSellerFlatRemove, FlagSellerRatiosAdd, FlagSellerRatiosRemove, + FlagBuyerFlatAdd, FlagBuyerFlatRemove, FlagBuyerRatiosAdd, FlagBuyerRatiosRemove, + FlagProposal, + ) + + AddUseArgs(cmd, + ReqFlagUse(FlagMarket, "market id"), + OptFlagUse(FlagAuthority, "authority"), + UseFlagsBreak, + OptFlagUse(FlagAskAdd, "coins"), + OptFlagUse(FlagAskRemove, "coins"), + UseFlagsBreak, + OptFlagUse(FlagBidAdd, "coins"), + OptFlagUse(FlagBidRemove, "coins"), + UseFlagsBreak, + OptFlagUse(FlagSellerFlatAdd, "coins"), + OptFlagUse(FlagSellerFlatRemove, "coins"), + UseFlagsBreak, + OptFlagUse(FlagSellerRatiosAdd, "fee ratios"), + OptFlagUse(FlagSellerRatiosRemove, "fee ratios"), + UseFlagsBreak, + OptFlagUse(FlagBuyerFlatAdd, "coins"), + OptFlagUse(FlagBuyerFlatRemove, "coins"), + UseFlagsBreak, + OptFlagUse(FlagBuyerRatiosAdd, "fee ratios"), + OptFlagUse(FlagBuyerRatiosRemove, "fee ratios"), + UseFlagsBreak, + OptFlagUse(FlagProposal, "json filename"), + ) + AddUseDetails(cmd, + AuthorityDesc, RepeatableDesc, FeeRatioDesc, + ProposalFileDesc(&exchange.MsgGovManageFeesRequest{}), + ) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgGovManageFees reads all the SetupCmdTxGovManageFees flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgGovManageFees(clientCtx client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgGovManageFeesRequest, error) { + var msg *exchange.MsgGovManageFeesRequest + + errs := make([]error, 15) + msg, errs[0] = ReadMsgGovManageFeesRequestFromProposalFlag(clientCtx, flagSet) + msg.Authority, errs[1] = ReadFlagAuthorityOrDefault(flagSet, msg.Authority) + msg.MarketId, errs[2] = ReadFlagUint32OrDefault(flagSet, FlagMarket, msg.MarketId) + msg.AddFeeCreateAskFlat, errs[3] = ReadFlatFeeFlag(flagSet, FlagAskAdd, msg.AddFeeCreateAskFlat) + msg.RemoveFeeCreateAskFlat, errs[4] = ReadFlatFeeFlag(flagSet, FlagAskRemove, msg.RemoveFeeCreateAskFlat) + msg.AddFeeCreateBidFlat, errs[5] = ReadFlatFeeFlag(flagSet, FlagBidAdd, msg.AddFeeCreateBidFlat) + msg.RemoveFeeCreateBidFlat, errs[6] = ReadFlatFeeFlag(flagSet, FlagBidRemove, msg.RemoveFeeCreateBidFlat) + msg.AddFeeSellerSettlementFlat, errs[7] = ReadFlatFeeFlag(flagSet, FlagSellerFlatAdd, msg.AddFeeSellerSettlementFlat) + msg.RemoveFeeSellerSettlementFlat, errs[8] = ReadFlatFeeFlag(flagSet, FlagSellerFlatRemove, msg.RemoveFeeSellerSettlementFlat) + msg.AddFeeSellerSettlementRatios, errs[9] = ReadFeeRatiosFlag(flagSet, FlagSellerRatiosAdd, msg.AddFeeSellerSettlementRatios) + msg.RemoveFeeSellerSettlementRatios, errs[10] = ReadFeeRatiosFlag(flagSet, FlagSellerRatiosRemove, msg.RemoveFeeSellerSettlementRatios) + msg.AddFeeBuyerSettlementFlat, errs[11] = ReadFlatFeeFlag(flagSet, FlagBuyerFlatAdd, msg.AddFeeBuyerSettlementFlat) + msg.RemoveFeeBuyerSettlementFlat, errs[12] = ReadFlatFeeFlag(flagSet, FlagBuyerFlatRemove, msg.RemoveFeeBuyerSettlementFlat) + msg.AddFeeBuyerSettlementRatios, errs[13] = ReadFeeRatiosFlag(flagSet, FlagBuyerRatiosAdd, msg.AddFeeBuyerSettlementRatios) + msg.RemoveFeeBuyerSettlementRatios, errs[14] = ReadFeeRatiosFlag(flagSet, FlagBuyerRatiosRemove, msg.RemoveFeeBuyerSettlementRatios) + + return msg, errors.Join(errs...) +} + +// SetupCmdTxGovUpdateParams adds all the flags needed for MakeMsgGovUpdateParams. +func SetupCmdTxGovUpdateParams(cmd *cobra.Command) { + cmd.Flags().String(FlagAuthority, "", "The authority address to use (defaults to the governance module account)") + cmd.Flags().Uint32(FlagDefault, 0, "The default split (required)") + cmd.Flags().StringSlice(FlagSplit, nil, "The denom-splits (repeatable)") + + MarkFlagsRequired(cmd, FlagDefault) + + AddUseArgs(cmd, + ReqFlagUse(FlagDefault, "amount"), + OptFlagUse(FlagSplit, "splits"), + OptFlagUse(FlagAuthority, "authority"), + ) + AddUseDetails(cmd, + AuthorityDesc, + RepeatableDesc, + `A has the format ":". +An is in basis points and is limited to 0 to 10,000 (both inclusive). + +Example : nhash:500`, + ) + + cmd.Args = cobra.NoArgs +} + +// MakeMsgGovUpdateParams reads all the SetupCmdTxGovUpdateParams flags and creates the desired Msg. +// Satisfies the msgMaker type. +func MakeMsgGovUpdateParams(_ client.Context, flagSet *pflag.FlagSet, _ []string) (*exchange.MsgGovUpdateParamsRequest, error) { + msg := &exchange.MsgGovUpdateParamsRequest{} + + errs := make([]error, 3) + msg.Authority, errs[0] = ReadFlagAuthority(flagSet) + msg.Params.DefaultSplit, errs[1] = flagSet.GetUint32(FlagDefault) + msg.Params.DenomSplits, errs[2] = ReadSplitsFlag(flagSet, FlagSplit) + + return msg, errors.Join(errs...) +} diff --git a/x/exchange/client/cli/tx_setup_test.go b/x/exchange/client/cli/tx_setup_test.go new file mode 100644 index 0000000000..d6fb13ce2b --- /dev/null +++ b/x/exchange/client/cli/tx_setup_test.go @@ -0,0 +1,1668 @@ +package cli_test + +import ( + "errors" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" +) + +// "cosmos1geex7m2pv3j8yetnwd047h6lta047h6ls98cgw" = sdk.AccAddress("FromAddress_________").String() +// "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn" = cli.AuthorityAddr.String() + +// txMakerTestDef is the definition of a tx maker func to be tested. +// +// R is the type of the sdk.Msg returned by the maker. +type txMakerTestDef[R sdk.Msg] struct { + // makerName is the name of the maker func being tested. + makerName string + // maker is the tx request maker func being tested. + maker func(clientCtx client.Context, flagSet *pflag.FlagSet, args []string) (R, error) + // setup is the command setup func that sets up a command so it has what's needed by the maker. + setup func(cmd *cobra.Command) +} + +// txMakerTestCase is a test case for a tx maker func. +// +// R is the type of the sdk.Msg returned by the maker. +type txMakerTestCase[R sdk.Msg] struct { + // name is a name for this test case. + name string + // clientCtx is the client context to provided to the maker. + clientCtx client.Context + // flags are the strings the give to FlagSet before it's provided to the maker. + flags []string + // args are the strings to supply as args to the maker. + args []string + // expMsg is the expected Msg result. + expMsg R + // expErr is the expected error string. An empty string indicates the error should be nil. + expErr string +} + +// runTxMakerTestCase runs a test case for a tx maker func. +// +// R is the type of the sdk.Msg returned by the maker. +func runTxMakerTestCase[R sdk.Msg](t *testing.T, td txMakerTestDef[R], tc txMakerTestCase[R]) { + cmd := &cobra.Command{ + Use: "dummy", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("this dummy command should not have been executed") + }, + } + cmd.Flags().String(flags.FlagFrom, "", "The from address") + td.setup(cmd) + + err := cmd.Flags().Parse(tc.flags) + require.NoError(t, err, "cmd.Flags().Parse(%q)", tc.flags) + + var msg R + testFunc := func() { + msg, err = td.maker(tc.clientCtx, cmd.Flags(), tc.args) + } + require.NotPanics(t, testFunc, td.makerName) + assertions.AssertErrorValue(t, err, tc.expErr, "%s error", td.makerName) + assert.Equal(t, tc.expMsg, msg, "%s msg", td.makerName) +} + +func TestSetupCmdTxCreateAsk(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxCreateAsk", + setup: cli.SetupCmdTxCreateAsk, + expFlags: []string{ + cli.FlagSeller, cli.FlagMarket, cli.FlagAssets, cli.FlagPrice, + cli.FlagSettlementFee, cli.FlagPartial, cli.FlagExternalID, cli.FlagCreationFee, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagSeller}}, + cli.FlagSeller: {oneReq: {flags.FlagFrom + " " + cli.FlagSeller}}, + cli.FlagMarket: {required: {"true"}}, + cli.FlagAssets: {required: {"true"}}, + cli.FlagPrice: {required: {"true"}}, + }, + expInUse: []string{ + "--seller", "--market ", "--assets ", "--price ", + "[--settlement-fee ]", "[--partial]", + "[--external-id ]", "[--creation-fee ]", + cli.ReqSignerDesc(cli.FlagSeller), + }, + }) +} + +func TestMakeMsgCreateAsk(t *testing.T) { + td := txMakerTestDef[*exchange.MsgCreateAskRequest]{ + makerName: "MakeMsgCreateAsk", + maker: cli.MakeMsgCreateAsk, + setup: cli.SetupCmdTxCreateAsk, + } + + tests := []txMakerTestCase[*exchange.MsgCreateAskRequest]{ + { + name: "a couple errors", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--assets", "nope", "--creation-fee", "123"}, + expMsg: &exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{Seller: sdk.AccAddress("FromAddress_________").String()}, + }, + expErr: joinErrs( + "error parsing --assets as a coin: invalid coin expression: \"nope\"", + "missing required --price flag", + "error parsing --creation-fee as a coin: invalid coin expression: \"123\"", + ), + }, + { + name: "all fields", + flags: []string{ + "--seller", "someaddr", "--market", "4", + "--assets", "10apple", "--price", "55plum", + "--settlement-fee", "5fig", "--partial", + "--external-id", "uuid", "--creation-fee", "6grape", + }, + expMsg: &exchange.MsgCreateAskRequest{ + AskOrder: exchange.AskOrder{ + MarketId: 4, + Seller: "someaddr", + Assets: sdk.NewInt64Coin("apple", 10), + Price: sdk.NewInt64Coin("plum", 55), + SellerSettlementFlatFee: &sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(5)}, + AllowPartial: true, + ExternalId: "uuid", + }, + OrderCreationFee: &sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(6)}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxCreateBid(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxCreateBid", + setup: cli.SetupCmdTxCreateBid, + expFlags: []string{ + cli.FlagBuyer, cli.FlagMarket, cli.FlagAssets, cli.FlagPrice, + cli.FlagSettlementFee, cli.FlagPartial, cli.FlagExternalID, cli.FlagCreationFee, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagBuyer}}, + cli.FlagBuyer: {oneReq: {flags.FlagFrom + " " + cli.FlagBuyer}}, + cli.FlagMarket: {required: {"true"}}, + cli.FlagAssets: {required: {"true"}}, + cli.FlagPrice: {required: {"true"}}, + }, + expInUse: []string{ + "--buyer", "--market ", "--assets ", "--price ", + "[--settlement-fee ]", "[--partial]", + "[--external-id ]", "[--creation-fee ]", + cli.ReqSignerDesc(cli.FlagBuyer), + }, + }) +} + +func TestMakeMsgCreateBid(t *testing.T) { + td := txMakerTestDef[*exchange.MsgCreateBidRequest]{ + makerName: "MakeMsgCreateBid", + maker: cli.MakeMsgCreateBid, + setup: cli.SetupCmdTxCreateBid, + } + + tests := []txMakerTestCase[*exchange.MsgCreateBidRequest]{ + { + name: "a couple errors", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--assets", "nope", "--creation-fee", "123"}, + expMsg: &exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{Buyer: sdk.AccAddress("FromAddress_________").String()}, + }, + expErr: joinErrs( + "error parsing --assets as a coin: invalid coin expression: \"nope\"", + "missing required --price flag", + "error parsing --creation-fee as a coin: invalid coin expression: \"123\"", + ), + }, + { + name: "all fields", + flags: []string{ + "--buyer", "someaddr", "--market", "4", + "--assets", "10apple", "--price", "55plum", + "--settlement-fee", "5fig", "--partial", + "--external-id", "uuid", "--creation-fee", "6grape", + }, + expMsg: &exchange.MsgCreateBidRequest{ + BidOrder: exchange.BidOrder{ + MarketId: 4, + Buyer: "someaddr", + Assets: sdk.NewInt64Coin("apple", 10), + Price: sdk.NewInt64Coin("plum", 55), + BuyerSettlementFees: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(5)}}, + AllowPartial: true, + ExternalId: "uuid", + }, + OrderCreationFee: &sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(6)}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxCancelOrder(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxCancelOrder", + setup: cli.SetupCmdTxCancelOrder, + expFlags: []string{ + cli.FlagSigner, cli.FlagOrder, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagSigner}}, + cli.FlagSigner: {oneReq: {flags.FlagFrom + " " + cli.FlagSigner}}, + }, + expInUse: []string{ + "{|--order }", + "{--from|--signer} ", + cli.ReqSignerDesc(cli.FlagSigner), + "The must be provided either as the first argument or using the --order flag, but not both.", + }, + }) +} + +func TestMakeMsgCancelOrder(t *testing.T) { + td := txMakerTestDef[*exchange.MsgCancelOrderRequest]{ + makerName: "MakeMsgCancelOrder", + maker: cli.MakeMsgCancelOrder, + setup: cli.SetupCmdTxCancelOrder, + } + + tests := []txMakerTestCase[*exchange.MsgCancelOrderRequest]{ + { + name: "nothing", + expMsg: &exchange.MsgCancelOrderRequest{}, + expErr: joinErrs( + "no provided", + "no provided", + ), + }, + { + name: "from and arg", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + args: []string{"87"}, + expMsg: &exchange.MsgCancelOrderRequest{ + Signer: sdk.AccAddress("FromAddress_________").String(), + OrderId: 87, + }, + }, + { + name: "signer and flag", + flags: []string{"--order", "52", "--signer", "someone"}, + expMsg: &exchange.MsgCancelOrderRequest{ + Signer: "someone", + OrderId: 52, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxFillBids(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxFillBids", + setup: cli.SetupCmdTxFillBids, + expFlags: []string{ + cli.FlagSeller, cli.FlagMarket, cli.FlagAssets, + cli.FlagBids, cli.FlagSettlementFee, cli.FlagCreationFee, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagSeller}}, + cli.FlagSeller: {oneReq: {flags.FlagFrom + " " + cli.FlagSeller}}, + cli.FlagMarket: {required: {"true"}}, + cli.FlagAssets: {required: {"true"}}, + cli.FlagBids: {required: {"true"}}, + }, + expInUse: []string{ + "{--from|--seller} ", "--market ", "--assets ", + "--bids ", "[--settlement-fee ]", + "[--creation-fee ]", + cli.ReqSignerDesc(cli.FlagSeller), + cli.RepeatableDesc, + }, + }) +} + +func TestMakeMsgFillBids(t *testing.T) { + td := txMakerTestDef[*exchange.MsgFillBidsRequest]{ + makerName: "MakeMsgFillBids", + maker: cli.MakeMsgFillBids, + setup: cli.SetupCmdTxFillBids, + } + + tests := []txMakerTestCase[*exchange.MsgFillBidsRequest]{ + { + name: "some errors", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--assets", "18", "--creation-fee", "apple"}, + expMsg: &exchange.MsgFillBidsRequest{ + Seller: sdk.AccAddress("FromAddress_________").String(), + }, + expErr: joinErrs( + "error parsing --assets as coins: invalid coin expression: \"18\"", + "error parsing --creation-fee as a coin: invalid coin expression: \"apple\"", + ), + }, + { + name: "all the flags", + flags: []string{ + "--market", "10", "--seller", "mike", + "--assets", "18acorn,5apple", "--bids", "83,52,99", + "--settlement-fee", "15fig", "--creation-fee", "9grape", + "--bids", "5", + }, + expMsg: &exchange.MsgFillBidsRequest{ + Seller: "mike", + MarketId: 10, + TotalAssets: sdk.NewCoins(sdk.NewInt64Coin("acorn", 18), sdk.NewInt64Coin("apple", 5)), + BidOrderIds: []uint64{83, 52, 99, 5}, + SellerSettlementFlatFee: &sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(15)}, + AskOrderCreationFee: &sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(9)}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxFillAsks(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxFillAsks", + setup: cli.SetupCmdTxFillAsks, + expFlags: []string{ + cli.FlagBuyer, cli.FlagMarket, cli.FlagPrice, + cli.FlagAsks, cli.FlagSettlementFee, cli.FlagCreationFee, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagBuyer}}, + cli.FlagBuyer: {oneReq: {flags.FlagFrom + " " + cli.FlagBuyer}}, + cli.FlagMarket: {required: {"true"}}, + cli.FlagPrice: {required: {"true"}}, + cli.FlagAsks: {required: {"true"}}, + }, + expInUse: []string{ + "{--from|--buyer} ", "--market ", "--price ", + "--asks ", "[--settlement-fee ]", + "[--creation-fee ]", + cli.ReqSignerDesc(cli.FlagBuyer), + cli.RepeatableDesc, + }, + }) +} + +func TestMakeMsgFillAsks(t *testing.T) { + td := txMakerTestDef[*exchange.MsgFillAsksRequest]{ + makerName: "MakeMsgFillAsks", + maker: cli.MakeMsgFillAsks, + setup: cli.SetupCmdTxFillAsks, + } + + tests := []txMakerTestCase[*exchange.MsgFillAsksRequest]{ + { + name: "some errors", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--price", "18", "--creation-fee", "apple"}, + expMsg: &exchange.MsgFillAsksRequest{ + Buyer: sdk.AccAddress("FromAddress_________").String(), + }, + expErr: joinErrs( + "error parsing --price as a coin: invalid coin expression: \"18\"", + "error parsing --creation-fee as a coin: invalid coin expression: \"apple\"", + ), + }, + { + name: "all the flags", + flags: []string{ + "--market", "10", "--buyer", "george", + "--price", "23apple", "--asks", "41,12,77", + "--settlement-fee", "15fig", "--creation-fee", "9grape", + "--asks", "20", "--asks", "987,444,6", + }, + expMsg: &exchange.MsgFillAsksRequest{ + Buyer: "george", + MarketId: 10, + TotalPrice: sdk.NewInt64Coin("apple", 23), + AskOrderIds: []uint64{41, 12, 77, 20, 987, 444, 6}, + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 15)), + BidOrderCreationFee: &sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(9)}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketSettle(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketSettle", + setup: cli.SetupCmdTxMarketSettle, + expFlags: []string{ + cli.FlagMarket, cli.FlagAsks, cli.FlagBids, cli.FlagPartial, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagMarket: {required: {"true"}}, + cli.FlagAsks: {required: {"true"}}, + cli.FlagBids: {required: {"true"}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", + "--asks ", "--bids ", + "[--partial]", + cli.ReqAdminDesc, cli.RepeatableDesc, + }, + }) +} + +func TestMakeMsgMarketSettle(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketSettleRequest]{ + makerName: "MakeMsgMarketSettle", + maker: cli.MakeMsgMarketSettle, + setup: cli.SetupCmdTxMarketSettle, + } + + tests := []txMakerTestCase[*exchange.MsgMarketSettleRequest]{ + { + name: "no admin", + flags: []string{"--asks", "7", "--bids", "8", "--partial"}, + expMsg: &exchange.MsgMarketSettleRequest{ + AskOrderIds: []uint64{7}, + BidOrderIds: []uint64{8}, + ExpectPartial: true, + }, + expErr: "no provided", + }, + { + name: "from", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--asks", "15,16,17", "--market", "52", "--bids", "51,52,53", + "--asks", "8", "--bids", "9"}, + expMsg: &exchange.MsgMarketSettleRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 52, + AskOrderIds: []uint64{15, 16, 17, 8}, + BidOrderIds: []uint64{51, 52, 53, 9}, + }, + }, + { + name: "authority", + flags: []string{"--market", "52", "--asks", "91", "--bids", "12,13", "--authority", "--partial"}, + expMsg: &exchange.MsgMarketSettleRequest{ + Admin: cli.AuthorityAddr.String(), + MarketId: 52, + AskOrderIds: []uint64{91}, + BidOrderIds: []uint64{12, 13}, + ExpectPartial: true, + }, + }, + { + name: "admin", + flags: []string{"--market", "14", "--admin", "bob", "--asks", "1,2,3", "--bids", "5"}, + expMsg: &exchange.MsgMarketSettleRequest{ + Admin: "bob", + MarketId: 14, + AskOrderIds: []uint64{1, 2, 3}, + BidOrderIds: []uint64{5}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketSetOrderExternalID(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketSetOrderExternalID", + setup: cli.SetupCmdTxMarketSetOrderExternalID, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, cli.FlagOrder, cli.FlagExternalID, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagOrder: {required: {"true"}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", "--order ", + "[--external-id ]", + cli.ReqAdminDesc, + }, + }) +} + +func TestMakeMsgMarketSetOrderExternalID(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketSetOrderExternalIDRequest]{ + makerName: "MakeMsgMarketSetOrderExternalID", + maker: cli.MakeMsgMarketSetOrderExternalID, + setup: cli.SetupCmdTxMarketSetOrderExternalID, + } + + tests := []txMakerTestCase[*exchange.MsgMarketSetOrderExternalIDRequest]{ + { + name: "no admin", + flags: []string{"--market", "8", "--order", "7", "--external-id", "markus"}, + expMsg: &exchange.MsgMarketSetOrderExternalIDRequest{ + MarketId: 8, OrderId: 7, ExternalId: "markus", + }, + expErr: "no provided", + }, + { + name: "no external id", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--market", "4000", "--order", "9001"}, + expMsg: &exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 4000, OrderId: 9001, ExternalId: "", + }, + }, + { + name: "all the flags", + flags: []string{ + "--market", "5", "--order", "100000000000001", + "--external-id", "one", "--admin", "michelle", + }, + expMsg: &exchange.MsgMarketSetOrderExternalIDRequest{ + Admin: "michelle", MarketId: 5, OrderId: 100000000000001, ExternalId: "one", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketWithdraw(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketWithdraw", + setup: cli.SetupCmdTxMarketWithdraw, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, cli.FlagTo, cli.FlagAmount, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagTo: {required: {"true"}}, + cli.FlagAmount: {required: {"true"}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", "--to ", "--amount ", + cli.ReqAdminDesc, + }, + }) +} + +func TestMakeMsgMarketWithdraw(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketWithdrawRequest]{ + makerName: "MakeMsgMarketWithdraw", + maker: cli.MakeMsgMarketWithdraw, + setup: cli.SetupCmdTxMarketWithdraw, + } + + tests := []txMakerTestCase[*exchange.MsgMarketWithdrawRequest]{ + { + name: "some errors", + flags: []string{"--market", "5", "--to", "annie", "--amount", "bill"}, + expMsg: &exchange.MsgMarketWithdrawRequest{ + Admin: "", MarketId: 5, ToAddress: "annie", Amount: nil, + }, + expErr: joinErrs( + "no provided", + "error parsing --amount as coins: invalid coin expression: \"bill\"", + ), + }, + { + name: "all fields", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--market", "2", "--to", "samantha", "--amount", "52plum,18pear"}, + expMsg: &exchange.MsgMarketWithdrawRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 2, ToAddress: "samantha", + Amount: sdk.NewCoins(sdk.NewInt64Coin("plum", 52), sdk.NewInt64Coin("pear", 18)), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestAddFlagsMarketDetails(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "AddFlagsMarketDetails", + setup: cli.AddFlagsMarketDetails, + expFlags: []string{cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon}, + skipArgsCheck: true, + }) +} + +func TestReadFlagsMarketDetails(t *testing.T) { + tests := []struct { + name string + skipSetup bool + flags []string + def exchange.MarketDetails + expDetails exchange.MarketDetails + expErr string + }{ + { + name: "no setup", + skipSetup: true, + expErr: joinErrs( + "flag accessed but not defined: name", + "flag accessed but not defined: description", + "flag accessed but not defined: url", + "flag accessed but not defined: icon", + ), + }, + { + name: "just name", + flags: []string{"--name", "Richard"}, + expDetails: exchange.MarketDetails{Name: "Richard"}, + }, + { + name: "name and url, no defaults", + flags: []string{"--url", "https://example.com", "--name", "Sally"}, + expDetails: exchange.MarketDetails{ + Name: "Sally", + WebsiteUrl: "https://example.com", + }, + }, + { + name: "name and url, with defaults", + flags: []string{"--url", "https://example.com/new", "--name", "Glen"}, + def: exchange.MarketDetails{ + Name: "Martha", + Description: "Some existing Description", + WebsiteUrl: "https://old.example.com", + IconUri: "https://example.com/icon", + }, + expDetails: exchange.MarketDetails{ + Name: "Glen", + Description: "Some existing Description", + WebsiteUrl: "https://example.com/new", + IconUri: "https://example.com/icon", + }, + }, + { + name: "all fields", + flags: []string{ + "--name", "Market Eight Dude", + "--description", "The Little Lebowski", + "--url", "https://bowling.god", + "--icon", "https://bowling.god/icon", + }, + expDetails: exchange.MarketDetails{ + Name: "Market Eight Dude", + Description: "The Little Lebowski", + WebsiteUrl: "https://bowling.god", + IconUri: "https://bowling.god/icon", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "dummy", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("this dummy command should not have been executed") + }, + } + if !tc.skipSetup { + cli.AddFlagsMarketDetails(cmd) + } + + err := cmd.Flags().Parse(tc.flags) + require.NoError(t, err, "cmd.Flags().Parse(%q)", tc.flags) + + var details exchange.MarketDetails + testFunc := func() { + details, err = cli.ReadFlagsMarketDetails(cmd.Flags(), tc.def) + } + require.NotPanics(t, testFunc, "ReadFlagsMarketDetails") + assertions.AssertErrorValue(t, err, tc.expErr, "ReadFlagsMarketDetails") + assert.Equal(t, tc.expDetails, details, "ReadFlagsMarketDetails") + }) + } +} + +func TestSetupCmdTxMarketUpdateDetails(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketUpdateDetails", + setup: cli.SetupCmdTxMarketUpdateDetails, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, + cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", + "[--name ]", "[--description ]", "[--url ]", "[--icon ]", + cli.ReqAdminDesc, + `All fields of a market's details will be updated. +If you omit an optional flag, that field will be updated to an empty string.`, + }, + }) +} + +func TestMakeMsgMarketUpdateDetails(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketUpdateDetailsRequest]{ + makerName: "MakeMsgMarketUpdateDetails", + maker: cli.MakeMsgMarketUpdateDetails, + setup: cli.SetupCmdTxMarketUpdateDetails, + } + + tests := []txMakerTestCase[*exchange.MsgMarketUpdateDetailsRequest]{ + { + name: "no admin", + flags: []string{"--market", "8", "--name", "Lynne"}, + expMsg: &exchange.MsgMarketUpdateDetailsRequest{ + MarketId: 8, + MarketDetails: exchange.MarketDetails{Name: "Lynne"}, + }, + expErr: "no provided", + }, + { + name: "just name and description", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--market", "9002", "--name", "River", "--description", "The person, not the water."}, + expMsg: &exchange.MsgMarketUpdateDetailsRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 9002, + MarketDetails: exchange.MarketDetails{ + Name: "River", + Description: "The person, not the water.", + }, + }, + }, + { + name: "all fields", + flags: []string{ + "--market", "14", "--authority", "--name", "Ashley", + "--icon", "https://example.com/ashley/icon", + "--url", "https://example.com/ashley", + "--description", "The best market out there.", + }, + expMsg: &exchange.MsgMarketUpdateDetailsRequest{ + Admin: cli.AuthorityAddr.String(), + MarketId: 14, + MarketDetails: exchange.MarketDetails{ + Name: "Ashley", + Description: "The best market out there.", + WebsiteUrl: "https://example.com/ashley", + IconUri: "https://example.com/ashley/icon", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketUpdateEnabled(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketUpdateEnabled", + setup: cli.SetupCmdTxMarketUpdateEnabled, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, cli.FlagEnable, cli.FlagDisable, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagEnable: { + mutExc: {cli.FlagEnable + " " + cli.FlagDisable}, + oneReq: {cli.FlagEnable + " " + cli.FlagDisable}, + }, + cli.FlagDisable: { + mutExc: {cli.FlagEnable + " " + cli.FlagDisable}, + oneReq: {cli.FlagEnable + " " + cli.FlagDisable}, + }, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", cli.ReqEnableDisableUse, + cli.ReqAdminDesc, cli.ReqEnableDisableDesc, + }, + }) +} + +func TestMakeMsgMarketUpdateEnabled(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketUpdateEnabledRequest]{ + makerName: "MakeMsgMarketUpdateEnabled", + maker: cli.MakeMsgMarketUpdateEnabled, + setup: cli.SetupCmdTxMarketUpdateEnabled, + } + + tests := []txMakerTestCase[*exchange.MsgMarketUpdateEnabledRequest]{ + { + name: "some errors", + flags: []string{"--market", "56"}, + expMsg: &exchange.MsgMarketUpdateEnabledRequest{MarketId: 56}, + expErr: joinErrs( + "no provided", + "exactly one of --enable or --disable must be provided", + ), + }, + { + name: "enable", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--enable", "--market", "4"}, + expMsg: &exchange.MsgMarketUpdateEnabledRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 4, + AcceptingOrders: true, + }, + }, + { + name: "disable", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--admin", "Blake", "--market", "94", "--disable"}, + expMsg: &exchange.MsgMarketUpdateEnabledRequest{ + Admin: "Blake", + MarketId: 94, + AcceptingOrders: false, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketUpdateUserSettle(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketUpdateUserSettle", + setup: cli.SetupCmdTxMarketUpdateUserSettle, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, cli.FlagEnable, cli.FlagDisable, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagEnable: { + mutExc: {cli.FlagEnable + " " + cli.FlagDisable}, + oneReq: {cli.FlagEnable + " " + cli.FlagDisable}, + }, + cli.FlagDisable: { + mutExc: {cli.FlagEnable + " " + cli.FlagDisable}, + oneReq: {cli.FlagEnable + " " + cli.FlagDisable}, + }, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", cli.ReqEnableDisableUse, + cli.ReqAdminDesc, cli.ReqEnableDisableDesc, + }, + }) +} + +func TestMakeMsgMarketUpdateUserSettle(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketUpdateUserSettleRequest]{ + makerName: "MakeMsgMarketUpdateUserSettle", + maker: cli.MakeMsgMarketUpdateUserSettle, + setup: cli.SetupCmdTxMarketUpdateUserSettle, + } + + tests := []txMakerTestCase[*exchange.MsgMarketUpdateUserSettleRequest]{ + { + name: "some errors", + flags: []string{"--market", "56"}, + expMsg: &exchange.MsgMarketUpdateUserSettleRequest{MarketId: 56}, + expErr: joinErrs( + "no provided", + "exactly one of --enable or --disable must be provided", + ), + }, + { + name: "enable", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--enable", "--market", "4"}, + expMsg: &exchange.MsgMarketUpdateUserSettleRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 4, + AllowUserSettlement: true, + }, + }, + { + name: "disable", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--admin", "Blake", "--market", "94", "--disable"}, + expMsg: &exchange.MsgMarketUpdateUserSettleRequest{ + Admin: "Blake", + MarketId: 94, + AllowUserSettlement: false, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketManagePermissions(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketManagePermissions", + setup: cli.SetupCmdTxMarketManagePermissions, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, + cli.FlagMarket, cli.FlagRevokeAll, cli.FlagRevoke, cli.FlagGrant, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagRevokeAll: {oneReq: {cli.FlagRevokeAll + " " + cli.FlagRevoke + " " + cli.FlagGrant}}, + cli.FlagRevoke: {oneReq: {cli.FlagRevokeAll + " " + cli.FlagRevoke + " " + cli.FlagGrant}}, + cli.FlagGrant: {oneReq: {cli.FlagRevokeAll + " " + cli.FlagRevoke + " " + cli.FlagGrant}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", + "[--revoke-all ]", "[--revoke ]", "[--grant ]", + cli.ReqAdminDesc, cli.RepeatableDesc, cli.AccessGrantsDesc, + }, + }) +} + +func TestMakeMsgMarketManagePermissions(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketManagePermissionsRequest]{ + makerName: "MakeMsgMarketManagePermissions", + maker: cli.MakeMsgMarketManagePermissions, + setup: cli.SetupCmdTxMarketManagePermissions, + } + + accessGrant := func(addr string, perms ...exchange.Permission) exchange.AccessGrant { + return exchange.AccessGrant{Address: addr, Permissions: perms} + } + tests := []txMakerTestCase[*exchange.MsgMarketManagePermissionsRequest]{ + { + name: "some errors", + flags: []string{"--revoke", "addr8:oops", "--revoke", "Ryan", "--market", "1", "--grant", ":settle"}, + expMsg: &exchange.MsgMarketManagePermissionsRequest{ + MarketId: 1, + RevokeAll: []string{}, + ToRevoke: []exchange.AccessGrant{}, + ToGrant: []exchange.AccessGrant{}, + }, + expErr: joinErrs( + "no provided", + "could not parse permissions for \"addr8\" from \"oops\": invalid permission: \"oops\"", + "could not parse \"Ryan\" as an : expected format
:", + "invalid \":settle\": both an
and are required", + ), + }, + { + name: "just a revoke", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--market", "6", "--revoke", "alan:settle+update"}, + expMsg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 6, + RevokeAll: []string{}, + ToRevoke: []exchange.AccessGrant{ + accessGrant("alan", exchange.Permission_settle, exchange.Permission_update), + }, + ToGrant: nil, + }, + }, + { + name: "all fields", + flags: []string{ + "--market", "123", "--admin", "Frankie", "--revoke-all", "Freddie,Fritz,Forrest", + "--revoke", "Dylan:settle,Devin:update", "--revoke-all", "Finn", + "--grant", "Sam:permissions+update", "--revoke", "Dave:setids", + "--grant", "Skylar:all,Fritz:all", + }, + expMsg: &exchange.MsgMarketManagePermissionsRequest{ + Admin: "Frankie", + MarketId: 123, + RevokeAll: []string{"Freddie", "Fritz", "Forrest", "Finn"}, + ToRevoke: []exchange.AccessGrant{ + accessGrant("Dylan", exchange.Permission_settle), + accessGrant("Devin", exchange.Permission_update), + accessGrant("Dave", exchange.Permission_set_ids), + }, + ToGrant: []exchange.AccessGrant{ + accessGrant("Sam", exchange.Permission_permissions, exchange.Permission_update), + accessGrant("Skylar", exchange.AllPermissions()...), + accessGrant("Fritz", exchange.AllPermissions()...), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxMarketManageReqAttrs(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxMarketManageReqAttrs", + setup: cli.SetupCmdTxMarketManageReqAttrs, + expFlags: []string{ + cli.FlagAdmin, cli.FlagAuthority, cli.FlagMarket, + cli.FlagAskAdd, cli.FlagAskRemove, cli.FlagBidAdd, cli.FlagBidRemove, + flags.FlagFrom, // not added by setup, but include so the annotation is checked. + }, + expAnnotations: map[string]map[string][]string{ + flags.FlagFrom: {oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}}, + cli.FlagAdmin: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagAuthority: { + mutExc: {cli.FlagAdmin + " " + cli.FlagAuthority}, + oneReq: {flags.FlagFrom + " " + cli.FlagAdmin + " " + cli.FlagAuthority}, + }, + cli.FlagMarket: {required: {"true"}}, + cli.FlagAskAdd: {oneReq: {cli.FlagAskAdd + " " + cli.FlagAskRemove + " " + cli.FlagBidAdd + " " + cli.FlagBidRemove}}, + cli.FlagAskRemove: {oneReq: {cli.FlagAskAdd + " " + cli.FlagAskRemove + " " + cli.FlagBidAdd + " " + cli.FlagBidRemove}}, + cli.FlagBidAdd: {oneReq: {cli.FlagAskAdd + " " + cli.FlagAskRemove + " " + cli.FlagBidAdd + " " + cli.FlagBidRemove}}, + cli.FlagBidRemove: {oneReq: {cli.FlagAskAdd + " " + cli.FlagAskRemove + " " + cli.FlagBidAdd + " " + cli.FlagBidRemove}}, + }, + expInUse: []string{ + cli.ReqAdminUse, "--market ", + "[--ask-add ]", "[--ask-remove ]", + "[--bid-add ]", "[--bid-remove ]", + cli.ReqAdminDesc, cli.RepeatableDesc, + }, + }) +} + +func TestMakeMsgMarketManageReqAttrs(t *testing.T) { + td := txMakerTestDef[*exchange.MsgMarketManageReqAttrsRequest]{ + makerName: "MakeMsgMarketManageReqAttrs", + maker: cli.MakeMsgMarketManageReqAttrs, + setup: cli.SetupCmdTxMarketManageReqAttrs, + } + + tests := []txMakerTestCase[*exchange.MsgMarketManageReqAttrsRequest]{ + { + name: "no admin", + flags: []string{"--market", "41", "--bid-add", "*.kyc"}, + expMsg: &exchange.MsgMarketManageReqAttrsRequest{ + MarketId: 41, + CreateAskToAdd: []string{}, + CreateAskToRemove: []string{}, + CreateBidToAdd: []string{"*.kyc"}, + CreateBidToRemove: []string{}, + }, + expErr: "no provided", + }, + { + name: "all fields", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--market", "44444", + "--ask-add", "def.abc,*.xyz", "--ask-remove", "uvw.xyz", + "--bid-add", "ghi.abc,*.xyz", "--bid-remove", "rst.xyz", + }, + expMsg: &exchange.MsgMarketManageReqAttrsRequest{ + Admin: sdk.AccAddress("FromAddress_________").String(), + MarketId: 44444, + CreateAskToAdd: []string{"def.abc", "*.xyz"}, + CreateAskToRemove: []string{"uvw.xyz"}, + CreateBidToAdd: []string{"ghi.abc", "*.xyz"}, + CreateBidToRemove: []string{"rst.xyz"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxGovCreateMarket(t *testing.T) { + tc := setupTestCase{ + name: "SetupCmdTxGovCreateMarket", + setup: cli.SetupCmdTxGovCreateMarket, + expFlags: []string{ + cli.FlagAuthority, + cli.FlagMarket, cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon, + cli.FlagCreateAsk, cli.FlagCreateBid, + cli.FlagSellerFlat, cli.FlagSellerRatios, cli.FlagBuyerFlat, cli.FlagBuyerRatios, + cli.FlagAcceptingOrders, cli.FlagAllowUserSettle, cli.FlagAccessGrants, + cli.FlagReqAttrAsk, cli.FlagReqAttrBid, + cli.FlagProposal, + }, + expInUse: []string{ + "[--authority ]", "[--market ]", + "[--name ]", "[--description ]", "[--url ]", "[--icon ]", + "[--create-ask ]", "[--create-bid ]", + "[--seller-flat ]", "[--seller-ratios ]", + "[--buyer-flat ]", "[--buyer-ratios ]", + "[--accepting-orders]", "[--allow-user-settle]", + "[--access-grants ]", + "[--req-attr-ask ]", "[--req-attr-bid ]", + "[--proposal ", + cli.AuthorityDesc, cli.RepeatableDesc, cli.AccessGrantsDesc, cli.FeeRatioDesc, + cli.ProposalFileDesc(&exchange.MsgGovCreateMarketRequest{}), + }, + } + + oneReqFlags := []string{ + cli.FlagMarket, cli.FlagName, cli.FlagDescription, cli.FlagURL, cli.FlagIcon, + cli.FlagCreateAsk, cli.FlagCreateBid, + cli.FlagSellerFlat, cli.FlagSellerRatios, cli.FlagBuyerFlat, cli.FlagBuyerRatios, + cli.FlagAcceptingOrders, cli.FlagAllowUserSettle, cli.FlagAccessGrants, + cli.FlagReqAttrAsk, cli.FlagReqAttrBid, + cli.FlagProposal, + } + oneReqVal := strings.Join(oneReqFlags, " ") + if tc.expAnnotations == nil { + tc.expAnnotations = make(map[string]map[string][]string) + } + for _, name := range oneReqFlags { + if tc.expAnnotations[name] == nil { + tc.expAnnotations[name] = make(map[string][]string) + } + tc.expAnnotations[name][oneReq] = []string{oneReqVal} + } + + runSetupTestCase(t, tc) +} + +func TestMakeMsgGovCreateMarket(t *testing.T) { + td := txMakerTestDef[*exchange.MsgGovCreateMarketRequest]{ + makerName: "MakeMsgGovCreateMarket", + maker: cli.MakeMsgGovCreateMarket, + setup: cli.SetupCmdTxGovCreateMarket, + } + + tdir := t.TempDir() + propFN := filepath.Join(tdir, "manage-fees-prop.json") + fileMsg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + MarketId: 3, + MarketDetails: exchange.MarketDetails{ + Name: "A Name", + Description: "A description.", + WebsiteUrl: "A URL", + IconUri: "An Icon", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("apple", 1)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 2)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 3)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grape", 110), Fee: sdk.NewInt64Coin("grape", 10)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("date", 4)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("kiwi", 111), Fee: sdk.NewInt64Coin("kiwi", 11)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("ag1_________________").String(), + Permissions: []exchange.Permission{2}, + }, + }, + ReqAttrCreateAsk: []string{"ask.create"}, + ReqAttrCreateBid: []string{"bid.create"}, + }, + } + prop := newGovProp(t, fileMsg) + tx := newTx(t, prop) + writeFileAsJson(t, propFN, tx) + + tests := []txMakerTestCase[*exchange.MsgGovCreateMarketRequest]{ + { + name: "several errors", + flags: []string{ + "--create-ask", "nope", "--seller-ratios", "8apple", + "--access-grants", "addr8:set", "--accepting-orders", + }, + expMsg: &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + FeeCreateAskFlat: []sdk.Coin{}, + FeeSellerSettlementRatios: []exchange.FeeRatio{}, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{}, + }, + }, + expErr: joinErrs( + "invalid coin expression: \"nope\"", + "cannot create FeeRatio from \"8apple\": expected exactly one colon", + "could not parse permissions for \"addr8\" from \"set\": invalid permission: \"set\"", + ), + }, + { + name: "all fields", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--market", "18", + "--create-ask", "10fig", "--create-bid", "5grape", + "--seller-flat", "12fig", "--seller-ratios", "100prune:1prune", + "--buyer-flat", "17fig", "--buyer-ratios", "88plum:3plum", + "--accepting-orders", "--allow-user-settle", + "--access-grants", "addr1:settle+cancel", "--access-grants", "addr2:update+permissions", + "--req-attr-ask", "seller.kyc", "--req-attr-bid", "buyer.kyc", + "--name", "Special market", "--description", "This market is special.", + "--url", "https://example.com", "--icon", "https://example.com/icon", + "--access-grants", "addr3:all", + }, + expMsg: &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + MarketId: 18, + MarketDetails: exchange.MarketDetails{ + Name: "Special market", + Description: "This market is special.", + WebsiteUrl: "https://example.com", + IconUri: "https://example.com/icon", + }, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 10)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("grape", 5)}, + FeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 12)}, + FeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 100), Fee: sdk.NewInt64Coin("prune", 1)}, + }, + FeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 17)}, + FeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("plum", 88), Fee: sdk.NewInt64Coin("plum", 3)}, + }, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: "addr1", + Permissions: []exchange.Permission{exchange.Permission_settle, exchange.Permission_cancel}, + }, + { + Address: "addr2", + Permissions: []exchange.Permission{exchange.Permission_update, exchange.Permission_permissions}, + }, + { + Address: "addr3", + Permissions: exchange.AllPermissions(), + }, + }, + ReqAttrCreateAsk: []string{"seller.kyc"}, + ReqAttrCreateBid: []string{"buyer.kyc"}, + }, + }, + }, + { + name: "proposal flag", + clientCtx: clientContextWithCodec(client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}), + flags: []string{"--proposal", propFN}, + expMsg: fileMsg, + }, + { + name: "proposal flag with others", + clientCtx: clientContextWithCodec(client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}), + flags: []string{"--proposal", propFN, "--market", "22"}, + expMsg: &exchange.MsgGovCreateMarketRequest{ + Authority: fileMsg.Authority, + Market: exchange.Market{ + MarketId: 22, + MarketDetails: fileMsg.Market.MarketDetails, + FeeCreateAskFlat: fileMsg.Market.FeeCreateAskFlat, + FeeCreateBidFlat: fileMsg.Market.FeeCreateBidFlat, + FeeSellerSettlementFlat: fileMsg.Market.FeeSellerSettlementFlat, + FeeSellerSettlementRatios: fileMsg.Market.FeeSellerSettlementRatios, + FeeBuyerSettlementFlat: fileMsg.Market.FeeBuyerSettlementFlat, + FeeBuyerSettlementRatios: fileMsg.Market.FeeBuyerSettlementRatios, + AcceptingOrders: fileMsg.Market.AcceptingOrders, + AllowUserSettlement: fileMsg.Market.AllowUserSettlement, + AccessGrants: fileMsg.Market.AccessGrants, + ReqAttrCreateAsk: fileMsg.Market.ReqAttrCreateAsk, + ReqAttrCreateBid: fileMsg.Market.ReqAttrCreateBid, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxGovManageFees(t *testing.T) { + tc := setupTestCase{ + name: "SetupCmdTxGovManageFees", + setup: cli.SetupCmdTxGovManageFees, + expFlags: []string{ + cli.FlagAuthority, cli.FlagMarket, + cli.FlagAskAdd, cli.FlagAskRemove, cli.FlagBidAdd, cli.FlagBidRemove, + cli.FlagSellerFlatAdd, cli.FlagSellerFlatRemove, cli.FlagSellerRatiosAdd, cli.FlagSellerRatiosRemove, + cli.FlagBuyerFlatAdd, cli.FlagBuyerFlatRemove, cli.FlagBuyerRatiosAdd, cli.FlagBuyerRatiosRemove, + cli.FlagProposal, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagMarket: {required: {"true"}}, + }, + expInUse: []string{ + "--market ", "[--authority ]", + "[--ask-add ]", "[--ask-remove ]", + "[--bid-add ]", "[--bid-remove ]", + "[--seller-flat-add ]", "[--seller-flat-remove ]", + "[--seller-ratios-add ]", "[--seller-ratios-remove ]", + "[--buyer-flat-add ]", "[--buyer-flat-remove ]", + "[--buyer-ratios-add ]", "[--buyer-ratios-remove ]", + "[--proposal ", + cli.AuthorityDesc, cli.RepeatableDesc, cli.FeeRatioDesc, + cli.ProposalFileDesc(&exchange.MsgGovManageFeesRequest{}), + }, + } + + oneReqFlags := []string{ + cli.FlagAskAdd, cli.FlagAskRemove, cli.FlagBidAdd, cli.FlagBidRemove, + cli.FlagSellerFlatAdd, cli.FlagSellerFlatRemove, cli.FlagSellerRatiosAdd, cli.FlagSellerRatiosRemove, + cli.FlagBuyerFlatAdd, cli.FlagBuyerFlatRemove, cli.FlagBuyerRatiosAdd, cli.FlagBuyerRatiosRemove, + cli.FlagProposal, + } + oneReqVal := strings.Join(oneReqFlags, " ") + if tc.expAnnotations == nil { + tc.expAnnotations = make(map[string]map[string][]string) + } + for _, name := range oneReqFlags { + if tc.expAnnotations[name] == nil { + tc.expAnnotations[name] = make(map[string][]string) + } + tc.expAnnotations[name][oneReq] = []string{oneReqVal} + } + + runSetupTestCase(t, tc) +} + +func TestMakeMsgGovManageFees(t *testing.T) { + td := txMakerTestDef[*exchange.MsgGovManageFeesRequest]{ + makerName: "MakeMsgGovManageFees", + maker: cli.MakeMsgGovManageFees, + setup: cli.SetupCmdTxGovManageFees, + } + + tdir := t.TempDir() + propFN := filepath.Join(tdir, "manage-fees-prop.json") + fileMsg := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 101, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("apple", 5)}, + RemoveFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("acorn", 6)}, + AddFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 7)}, + RemoveFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("blueberry", 8)}, + AddFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cherry", 9)}, + RemoveFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("cantaloupe", 10)}, + AddFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grape", 100), Fee: sdk.NewInt64Coin("grape", 1)}, + }, + RemoveFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("grapefruit", 101), Fee: sdk.NewInt64Coin("grapefruit", 2)}, + }, + AddFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("date", 11)}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("damson", 12)}, + AddFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("kiwi", 102), Fee: sdk.NewInt64Coin("kiwi", 3)}, + }, + RemoveFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("keylime", 104), Fee: sdk.NewInt64Coin("keylime", 4)}, + }, + } + prop := newGovProp(t, fileMsg) + tx := newTx(t, prop) + writeFileAsJson(t, propFN, tx) + + tests := []txMakerTestCase[*exchange.MsgGovManageFeesRequest]{ + { + name: "multiple errors", + flags: []string{ + "--ask-add", "15", "--buyer-flat-remove", "noamt", + }, + expMsg: &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + AddFeeCreateAskFlat: []sdk.Coin{}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{}, + }, + expErr: joinErrs( + "invalid coin expression: \"15\"", + "invalid coin expression: \"noamt\"", + ), + }, + { + name: "all fields", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--market", "55", + "--ask-add", "18fig", "--ask-remove", "15fig", "--ask-add", "5grape", + "--bid-add", "17fig", "--bid-remove", "14fig", + "--seller-flat-add", "55prune", "--seller-flat-remove", "54prune", + "--seller-ratios-add", "101prune:7prune", "--seller-ratios-remove", "101prune:3prune", + "--buyer-flat-add", "59prune", "--buyer-flat-remove", "57prune", + "--buyer-ratios-add", "107prune:1prune", "--buyer-ratios-remove", "43prune:2prune", + }, + expMsg: &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 55, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 18), sdk.NewInt64Coin("grape", 5)}, + RemoveFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 15)}, + AddFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 17)}, + RemoveFeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("fig", 14)}, + AddFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 55)}, + RemoveFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 54)}, + AddFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 101), Fee: sdk.NewInt64Coin("prune", 7)}, + }, + RemoveFeeSellerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 101), Fee: sdk.NewInt64Coin("prune", 3)}, + }, + AddFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 59)}, + RemoveFeeBuyerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("prune", 57)}, + AddFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 107), Fee: sdk.NewInt64Coin("prune", 1)}, + }, + RemoveFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("prune", 43), Fee: sdk.NewInt64Coin("prune", 2)}, + }, + }, + }, + { + name: "proposal flag", + clientCtx: clientContextWithCodec(client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}), + flags: []string{"--proposal", propFN}, + expMsg: fileMsg, + }, + { + name: "proposal flag plus others", + clientCtx: clientContextWithCodec(client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}), + flags: []string{"--market", "5", "--proposal", propFN}, + expMsg: &exchange.MsgGovManageFeesRequest{ + Authority: fileMsg.Authority, + MarketId: 5, + AddFeeCreateAskFlat: fileMsg.AddFeeCreateAskFlat, + RemoveFeeCreateAskFlat: fileMsg.RemoveFeeCreateAskFlat, + AddFeeCreateBidFlat: fileMsg.AddFeeCreateBidFlat, + RemoveFeeCreateBidFlat: fileMsg.RemoveFeeCreateBidFlat, + AddFeeSellerSettlementFlat: fileMsg.AddFeeSellerSettlementFlat, + RemoveFeeSellerSettlementFlat: fileMsg.RemoveFeeSellerSettlementFlat, + AddFeeSellerSettlementRatios: fileMsg.AddFeeSellerSettlementRatios, + RemoveFeeSellerSettlementRatios: fileMsg.RemoveFeeSellerSettlementRatios, + AddFeeBuyerSettlementFlat: fileMsg.AddFeeBuyerSettlementFlat, + RemoveFeeBuyerSettlementFlat: fileMsg.RemoveFeeBuyerSettlementFlat, + AddFeeBuyerSettlementRatios: fileMsg.AddFeeBuyerSettlementRatios, + RemoveFeeBuyerSettlementRatios: fileMsg.RemoveFeeBuyerSettlementRatios, + }, + expErr: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} + +func TestSetupCmdTxGovUpdateParams(t *testing.T) { + runSetupTestCase(t, setupTestCase{ + name: "SetupCmdTxGovUpdateParams", + setup: cli.SetupCmdTxGovUpdateParams, + expFlags: []string{ + cli.FlagAuthority, cli.FlagDefault, cli.FlagSplit, + }, + expAnnotations: map[string]map[string][]string{ + cli.FlagDefault: {required: {"true"}}, + }, + expInUse: []string{ + "--default ", "[--split ]", "[--authority ]", + cli.AuthorityDesc, cli.RepeatableDesc, + `A has the format ":". +An is in basis points and is limited to 0 to 10,000 (both inclusive). + +Example : nhash:500`, + }, + }) +} + +func TestMakeMsgGovUpdateParams(t *testing.T) { + td := txMakerTestDef[*exchange.MsgGovUpdateParamsRequest]{ + makerName: "MakeMsgGovUpdateParams", + maker: cli.MakeMsgGovUpdateParams, + setup: cli.SetupCmdTxGovUpdateParams, + } + + tests := []txMakerTestCase[*exchange.MsgGovUpdateParamsRequest]{ + { + name: "some errors", + flags: []string{"--split", "jack,14"}, + expMsg: &exchange.MsgGovUpdateParamsRequest{ + Authority: cli.AuthorityAddr.String(), + Params: exchange.Params{DenomSplits: []exchange.DenomSplit{}}, + }, + expErr: joinErrs( + "invalid denom split \"jack\": expected format :", + "invalid denom split \"14\": expected format :", + ), + }, + { + name: "no splits", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{"--default", "501"}, + expMsg: &exchange.MsgGovUpdateParamsRequest{ + Authority: cli.AuthorityAddr.String(), + Params: exchange.Params{DefaultSplit: 501}, + }, + }, + { + name: "all fields", + clientCtx: client.Context{FromAddress: sdk.AccAddress("FromAddress_________")}, + flags: []string{ + "--split", "banana:99", "--default", "105", + "--authority", "Jeff", "--split", "apple:333,plum:555"}, + expMsg: &exchange.MsgGovUpdateParamsRequest{ + Authority: "Jeff", + Params: exchange.Params{ + DefaultSplit: 105, + DenomSplits: []exchange.DenomSplit{ + {Denom: "banana", Split: 99}, + {Denom: "apple", Split: 333}, + {Denom: "plum", Split: 555}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runTxMakerTestCase(t, td, tc) + }) + } +} diff --git a/x/exchange/client/cli/tx_test.go b/x/exchange/client/cli/tx_test.go new file mode 100644 index 0000000000..b51ceddd90 --- /dev/null +++ b/x/exchange/client/cli/tx_test.go @@ -0,0 +1,941 @@ +package cli_test + +import ( + "bytes" + "sort" + + "golang.org/x/exp/maps" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/provenance-io/provenance/x/exchange" + "github.com/provenance-io/provenance/x/exchange/client/cli" +) + +var ( + // invReqCode is the TxResponse code for an ErrInvalidRequest. + invReqCode = sdkerrors.ErrInvalidRequest.ABCICode() + // invSigCode is the TxResponse code for an ErrInvalidSigner. + invSigCode = govtypes.ErrInvalidSigner.ABCICode() +) + +func (s *CmdTestSuite) TestCmdTxCreateAsk() { + tests := []txCmdTestCase{ + { + name: "cmd error", + args: []string{"create-ask", "--market", "3", + "--assets", "10apple", "--price", "20peach", + }, + expInErr: []string{"at least one of the flags in the group [from seller] is required"}, + }, + { + name: "insufficient creation fee", + args: []string{"create-ask", "--market", "3", + "--assets", "1000apple", "--price", "2000peach", + "--settlement-fee", "50peach", + "--creation-fee", "9peach", + "--from", s.addr2.String(), + }, + expInRawLog: []string{"failed to execute message", "invalid request", + "insufficient ask order creation fee: \"9peach\" is less than required amount \"10peach\""}, + expectedCode: invReqCode, + }, + { + name: "okay", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + expOrder := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 3, + Seller: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 1000), + Price: sdk.NewInt64Coin("peach", 2000), + SellerSettlementFlatFee: &sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(50)}, + AllowPartial: true, + ExternalId: "my-new-ask-order-E2DF6AFE", + }) + return nil, s.createOrderFollowup(expOrder) + }, + args: []string{"ask", "--market", "3", "--partial", + "--assets", "1000apple", "--price", "2000peach", + "--settlement-fee", "50peach", + "--creation-fee", "10peach", + "--from", s.addr2.String(), + "--external-id", "my-new-ask-order-E2DF6AFE", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxCreateBid() { + tests := []txCmdTestCase{ + { + name: "cmd error", + args: []string{"create-bid", "--market", "5", + "--assets", "10apple", "--price", "20peach", + }, + expInErr: []string{"at least one of the flags in the group [from buyer] is required"}, + }, + { + name: "insufficient creation fee", + args: []string{"create-bid", "--market", "5", + "--assets", "1000apple", "--price", "2000peach", + "--settlement-fee", "70peach", + "--creation-fee", "9peach", + "--from", s.addr2.String(), + }, + expInRawLog: []string{"failed to execute message", "invalid request", + "insufficient bid order creation fee: \"9peach\" is less than required amount \"10peach\""}, + expectedCode: invReqCode, + }, + { + name: "okay", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + expOrder := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 1000), + Price: sdk.NewInt64Coin("peach", 2000), + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("peach", 70)), + AllowPartial: true, + ExternalId: "my-new-bid-order-83A99979", + }) + return nil, s.createOrderFollowup(expOrder) + }, + args: []string{"bid", "--market", "5", "--partial", + "--assets", "1000apple", "--price", "2000peach", + "--settlement-fee", "70peach", + "--creation-fee", "10peach", + "--from", s.addr2.String(), + "--external-id", "my-new-bid-order-83A99979", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxCancelOrder() { + tests := []txCmdTestCase{ + { + name: "no order id", + args: []string{"cancel-order", "--from", s.addr2.String()}, + expInErr: []string{"no provided"}, + }, + { + name: "order does not exist", + args: []string{"cancel", "18446744073709551615", "--from", s.addr2.String()}, + expInRawLog: []string{"failed to execute message", "invalid request", + "order 18446744073709551615 does not exist"}, + expectedCode: invReqCode, + }, + { + name: "order exists", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + newOrder := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 100), + Price: sdk.NewInt64Coin("peach", 150), + }) + orderID := s.createOrder(newOrder, nil) + orderIDStr := orderIDStringer(orderID) + + return []string{"--order", orderIDStr}, s.getOrderFollowup(orderIDStr, nil) + }, + args: []string{"cancel", "--from", s.addr2.String()}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxFillBids() { + tests := []txCmdTestCase{ + { + name: "no bids", + args: []string{"fill-bids", "--from", s.addr3.String(), "--market", "5", "--assets", "100apple"}, + expInErr: []string{"required flag(s) \"bids\" not set"}, + }, + { + name: "ask order id provided", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + askOrder := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 100), + Price: sdk.NewInt64Coin("peach", 150), + }) + orderID := s.createOrder(askOrder, &sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}) + return []string{"--bids", orderIDStringer(orderID)}, nil + }, + args: []string{"fill-bids", "--from", s.addr3.String(), "--market", "5", "--assets", "100apple"}, + expInRawLog: []string{"failed to execute message", "invalid request", "is type ask: expected bid"}, + expectedCode: invReqCode, + }, + { + name: "two bids", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + creationFee := sdk.NewInt64Coin("peach", 10) + bid2 := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 1000), + Price: sdk.NewInt64Coin("peach", 1500), + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("peach", 65)), + }) + bid3 := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: s.addr3.String(), + Assets: sdk.NewInt64Coin("apple", 500), + Price: sdk.NewInt64Coin("peach", 1000), + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("peach", 60)), + }) + bid2ID := s.createOrder(bid2, &creationFee) + bid3ID := s.createOrder(bid3, &creationFee) + + preBalsAddr2 := s.queryBankBalances(s.addr2.String()) + preBalsAddr3 := s.queryBankBalances(s.addr3.String()) + preBalsAddr4 := s.queryBankBalances(s.addr4.String()) + + expBals := []banktypes.Balance{ + s.adjustBalance(preBalsAddr2, bid2), + s.adjustBalance(preBalsAddr3, bid3), + { + Address: s.addr4.String(), + Coins: preBalsAddr4.Add(bid2.GetPrice()).Add(bid3.GetPrice()). + Sub(bid2.GetAssets()).Sub(bid3.GetAssets()).Sub(s.bondCoins(10)...), + }, + } + + args := []string{"--bids", orderIDStringer(bid2ID) + "," + orderIDStringer(bid3ID)} + return args, s.assertBalancesFollowup(expBals) + }, + args: []string{"fill-bids", "--from", s.addr4.String(), "--market", "5", "--assets", "1500apple"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxFillAsks() { + tests := []txCmdTestCase{ + { + name: "no asks", + args: []string{"fill-asks", "--from", s.addr3.String(), "--market", "5", "--price", "100peach"}, + expInErr: []string{"required flag(s) \"asks\" not set"}, + }, + { + name: "bid order id provided", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + bidOrder := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 3, + Buyer: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 100), + Price: sdk.NewInt64Coin("peach", 150), + }) + orderID := s.createOrder(bidOrder, &sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}) + return []string{"--asks", orderIDStringer(orderID)}, nil + }, + args: []string{"fill-asks", "--from", s.addr3.String(), "--market", "3", "--price", "150peach"}, + expInRawLog: []string{"failed to execute message", "invalid request", "is type bid: expected ask"}, + expectedCode: invReqCode, + }, + { + name: "two asks", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + ask2 := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr2.String(), + Assets: sdk.NewInt64Coin("apple", 1000), + Price: sdk.NewInt64Coin("peach", 1500), + }) + ask3 := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr3.String(), + Assets: sdk.NewInt64Coin("apple", 500), + Price: sdk.NewInt64Coin("peach", 1000), + }) + ask2ID := s.createOrder(ask2, nil) + ask3ID := s.createOrder(ask3, nil) + + preBalsAddr2 := s.queryBankBalances(s.addr2.String()) + preBalsAddr3 := s.queryBankBalances(s.addr3.String()) + preBalsAddr4 := s.queryBankBalances(s.addr4.String()) + + expBals := []banktypes.Balance{ + s.adjustBalance(preBalsAddr2, ask2), + s.adjustBalance(preBalsAddr3, ask3), + { + Address: s.addr4.String(), + Coins: preBalsAddr4.Add(ask2.GetAssets()).Add(ask3.GetAssets()). + Sub(ask2.GetPrice()).Sub(ask3.GetPrice()). + Sub(sdk.NewInt64Coin("peach", 85)).Sub(s.bondCoins(10)...), + }, + } + + args := []string{"--asks", orderIDStringer(ask2ID) + "," + orderIDStringer(ask3ID)} + return args, s.assertBalancesFollowup(expBals) + }, + args: []string{"fill-asks", "--from", s.addr4.String(), "--market", "5", + "--price", "2500peach", "--settlement-fee", "75peach", "--creation-fee", "10peach"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketSettle() { + tests := []txCmdTestCase{ + { + name: "no asks", + args: []string{"market-settle", "--from", s.addr1.String(), "--market", "5", "--bids", "112,113"}, + expInErr: []string{"required flag(s) \"asks\" not set"}, + }, + { + name: "endpoint error", + args: []string{"market-settle", "--from", s.addr9.String(), "--market", "419", "--bids", "18446744073709551614", "--asks", "18446744073709551615"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr9.String() + " does not have permission to settle orders for market 419", + }, + expectedCode: invReqCode, + }, + { + name: "two asks two bids", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + creationFee := sdk.NewInt64Coin("peach", 10) + ask5 := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr5.String(), + Assets: sdk.NewInt64Coin("apple", 1000), + Price: sdk.NewInt64Coin("peach", 1500), + }) + ask6 := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr6.String(), + Assets: sdk.NewInt64Coin("apple", 500), + Price: sdk.NewInt64Coin("peach", 1000), + }) + bid7 := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: s.addr7.String(), + Assets: sdk.NewInt64Coin("apple", 700), + Price: sdk.NewInt64Coin("peach", 1300), + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("peach", 63)), + }) + bid8 := exchange.NewOrder(1).WithBid(&exchange.BidOrder{ + MarketId: 5, + Buyer: s.addr8.String(), + Assets: sdk.NewInt64Coin("apple", 800), + Price: sdk.NewInt64Coin("peach", 1200), + BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("peach", 62)), + }) + ask5ID := s.createOrder(ask5, nil) + ask6ID := s.createOrder(ask6, nil) + bid7ID := s.createOrder(bid7, &creationFee) + bid8ID := s.createOrder(bid8, &creationFee) + + preBalsAddr5 := s.queryBankBalances(s.addr5.String()) + preBalsAddr6 := s.queryBankBalances(s.addr6.String()) + preBalsAddr7 := s.queryBankBalances(s.addr7.String()) + preBalsAddr8 := s.queryBankBalances(s.addr8.String()) + + expBals := []banktypes.Balance{ + s.adjustBalance(preBalsAddr5, ask5), + s.adjustBalance(preBalsAddr6, ask6), + s.adjustBalance(preBalsAddr7, bid7), + s.adjustBalance(preBalsAddr8, bid8), + } + + args := []string{ + "--asks", orderIDStringer(ask5ID) + "," + orderIDStringer(ask6ID), + "--bids", orderIDStringer(bid7ID) + "," + orderIDStringer(bid8ID), + } + return args, s.assertBalancesFollowup(expBals) + }, + args: []string{"settle", "--from", s.addr1.String(), "--market", "5"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketSetOrderExternalID() { + tests := []txCmdTestCase{ + { + name: "no market id", + args: []string{"market-set-external-id", "--from", s.addr1.String(), + "--order", "10", "--external-id", "FD6A9038-E15F-4309-ADA6-1AAC3B09DD3E"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "does not have permission", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + newOrder := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr7.String(), + Assets: sdk.NewInt64Coin("apple", 100), + Price: sdk.NewInt64Coin("peach", 100), + ExternalId: "0A66B2C8-40EF-457A-95B8-5B1D41D020F9", + }) + orderID := s.createOrder(newOrder, nil) + orderIDStr := orderIDStringer(orderID) + + return []string{"--order", orderIDStr}, s.getOrderFollowup(orderIDStr, newOrder) + }, + args: []string{"set-external-id", "--market", "5", "--from", s.addr7.String(), + "--external-id", "984C9430-7E5E-461A-8468-1F067E26CBE9"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr7.String() + " does not have permission to set external ids on orders for market 5", + }, + expectedCode: invReqCode, + }, + { + name: "external id updated", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + newOrder := exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 5, + Seller: s.addr7.String(), + Assets: sdk.NewInt64Coin("apple", 100), + Price: sdk.NewInt64Coin("peach", 100), + ExternalId: "C0CC7021-A28B-4312-92C9-78DFADC68799", + }) + orderID := s.createOrder(newOrder, nil) + orderIDStr := orderIDStringer(orderID) + newOrder.GetAskOrder().ExternalId = "FF1C3210-D015-4EF8-A397-139E98602365" + + return []string{"--order", orderIDStr}, s.getOrderFollowup(orderIDStr, newOrder) + }, + args: []string{"market-set-order-external-id", "--from", s.addr1.String(), "--market", "5", + "--external-id", "FF1C3210-D015-4EF8-A397-139E98602365"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketWithdraw() { + tests := []txCmdTestCase{ + { + name: "no market id", + args: []string{"market-withdraw", "--from", s.addr1.String(), + "--to", s.addr1.String(), "--amount", "10peach"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "not enough in market account", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + market3Addr := exchange.GetMarketAddress(3) + expBals := []banktypes.Balance{ + {Address: market3Addr.String(), Coins: s.queryBankBalances(market3Addr.String())}, + {Address: s.addr2.String(), Coins: s.queryBankBalances(s.addr2.String())}, + } + + return nil, s.assertBalancesFollowup(expBals) + }, + args: []string{"market-withdraw", "--from", s.addr1.String(), + "--market", "3", "--to", s.addr2.String(), "--amount", "50avocado"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "failed to withdraw 50avocado from market 3", + "spendable balance 0avocado is smaller than 50avocado", + "insufficient funds", + }, + expectedCode: invReqCode, + }, + { + name: "success", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + amount := sdk.NewInt64Coin("acorn", 50) + market3Addr := exchange.GetMarketAddress(3) + s.execBankSend(s.addr8.String(), market3Addr.String(), amount.String()) + + preBalsMarket3 := s.queryBankBalances(market3Addr.String()) + preBalsAddr8 := s.queryBankBalances(s.addr8.String()) + + expBals := []banktypes.Balance{ + {Address: market3Addr.String(), Coins: preBalsMarket3.Sub(amount)}, + {Address: s.addr8.String(), Coins: preBalsAddr8.Add(amount)}, + } + + return []string{"--amount", amount.String()}, s.assertBalancesFollowup(expBals) + }, + args: []string{"withdraw", "--market", "3", "--from", s.addr1.String(), "--to", s.addr8.String()}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketUpdateDetails() { + tests := []txCmdTestCase{ + { + name: "no market", + args: []string{"market-details", "--from", s.addr1.String(), "--name", "Notgonnawork"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "market does not exist", + args: []string{"market-update-details", "--market", "419", + "--from", s.addr1.String(), "--name", "No Such Market"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr1.String() + " does not have permission to update market 419", + }, + expectedCode: invReqCode, + }, + { + name: "success", + preRun: func() ([]string, func(txResponse *sdk.TxResponse)) { + market3 := s.getMarket("3") + if len(market3.MarketDetails.IconUri) == 0 { + market3.MarketDetails.IconUri = "https://example.com/3/icon" + } + market3.MarketDetails.IconUri += "?7A9AF177=true" + + args := make([]string, 0, 8) + if len(market3.MarketDetails.Name) > 0 { + args = append(args, "--name", market3.MarketDetails.Name) + } + if len(market3.MarketDetails.Description) > 0 { + args = append(args, "--description", market3.MarketDetails.Description) + } + if len(market3.MarketDetails.WebsiteUrl) > 0 { + args = append(args, "--url", market3.MarketDetails.WebsiteUrl) + } + if len(market3.MarketDetails.IconUri) > 0 { + args = append(args, "--icon", market3.MarketDetails.IconUri) + } + + return args, s.getMarketFollowup("3", market3) + }, + args: []string{"market-update-details", "--from", s.addr1.String(), "--market", "3"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketUpdateEnabled() { + tests := []txCmdTestCase{ + { + name: "no market", + args: []string{"market-enabled", "--from", s.addr1.String(), "--enable"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "market does not exist", + args: []string{"market-update-enabled", "--market", "419", + "--from", s.addr4.String(), "--enable"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr4.String() + " does not have permission to update market 419", + }, + expectedCode: invReqCode, + }, + { + name: "disable market", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.AcceptingOrders = false + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"update-enabled", "--disable", "--market", "420", "--from", s.addr1.String()}, + expectedCode: 0, + }, + { + name: "enable market", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.AcceptingOrders = true + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"update-enabled", "--enable", "--market", "420", "--from", s.addr1.String()}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketUpdateUserSettle() { + tests := []txCmdTestCase{ + { + name: "no market", + args: []string{"market-user-settle", "--from", s.addr1.String(), "--enable"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "market does not exist", + args: []string{"market-update-user-settle", "--market", "419", + "--from", s.addr4.String(), "--enable"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr4.String() + " does not have permission to update market 419", + }, + expectedCode: invReqCode, + }, + { + name: "disable user settle", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.AllowUserSettlement = false + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"update-user-settle", "--disable", "--market", "420", "--from", s.addr1.String()}, + expectedCode: 0, + }, + { + name: "enable user settle", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.AllowUserSettlement = true + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"update-user-settle", "--enable", "--market", "420", "--from", s.addr1.String()}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketManagePermissions() { + tests := []txCmdTestCase{ + { + name: "no market", + args: []string{"market-permissions", "--from", s.addr1.String(), "--revoke-all", s.addr8.String()}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "market does not exist", + args: []string{"market-manage-permissions", "--market", "419", + "--from", s.addr4.String(), "--revoke-all", s.addr2.String()}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr4.String() + " does not have permission to manage permissions for market 419", + }, + expectedCode: invReqCode, + }, + { + name: "permissions updated", + preRun: func() ([]string, func(*sdk.TxResponse)) { + expPerms := map[int][]exchange.Permission{ + 1: exchange.AllPermissions(), + 4: {exchange.Permission_permissions}, + } + for _, perm := range exchange.AllPermissions() { + if perm != exchange.Permission_cancel { + expPerms[2] = append(expPerms[2], perm) + } + } + + addrOrder := maps.Keys(expPerms) + sort.Slice(addrOrder, func(i, j int) bool { + return bytes.Compare(s.accountAddrs[addrOrder[i]], s.accountAddrs[addrOrder[j]]) < 0 + }) + + market3 := s.getMarket("3") + market3.AccessGrants = []exchange.AccessGrant{} + for _, addrI := range addrOrder { + market3.AccessGrants = append(market3.AccessGrants, exchange.AccessGrant{ + Address: s.accountAddrs[addrI].String(), + Permissions: expPerms[addrI], + }) + } + + return nil, s.getMarketFollowup("3", market3) + }, + args: []string{ + "permissions", "--market", "3", "--from", s.addr1.String(), + "--revoke-all", s.addr3.String(), "--revoke", s.addr2.String() + ":cancel", + "--grant", s.addr4.String() + ":permissions", + }, + expectedCode: 0, + }, + { + name: "permissions put back", + preRun: func() ([]string, func(*sdk.TxResponse)) { + expPerms := map[int][]exchange.Permission{ + 1: exchange.AllPermissions(), + 2: exchange.AllPermissions(), + 3: {exchange.Permission_cancel, exchange.Permission_attributes}, + } + + addrOrder := maps.Keys(expPerms) + sort.Slice(addrOrder, func(i, j int) bool { + return bytes.Compare(s.accountAddrs[addrOrder[i]], s.accountAddrs[addrOrder[j]]) < 0 + }) + + market3 := s.getMarket("3") + market3.AccessGrants = []exchange.AccessGrant{} + for _, addrI := range addrOrder { + market3.AccessGrants = append(market3.AccessGrants, exchange.AccessGrant{ + Address: s.accountAddrs[addrI].String(), + Permissions: expPerms[addrI], + }) + } + + return nil, s.getMarketFollowup("3", market3) + }, + args: []string{ + "permissions", "--market", "3", "--from", s.addr4.String(), + "--revoke-all", s.addr2.String() + "," + s.addr4.String(), + "--grant", s.addr2.String() + ":all", + "--grant", s.addr3.String() + ":cancel+attributes", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxMarketManageReqAttrs() { + tests := []txCmdTestCase{ + { + name: "no market", + args: []string{"market-req-attrs", "--from", s.addr1.String(), "--ask-add", "*.nope"}, + expInErr: []string{"required flag(s) \"market\" not set"}, + }, + { + name: "market does not exist", + args: []string{"market-manage-req-attrs", "--market", "419", + "--from", s.addr4.String(), "--bid-add", "*.also.nope"}, + expInRawLog: []string{"failed to execute message", "invalid request", + "account " + s.addr4.String() + " does not have permission to manage required attributes for market 419", + }, + expectedCode: invReqCode, + }, + { + name: "req attrs updated", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.ReqAttrCreateAsk = []string{"seller.kyc", "*.my.attr"} + market420.ReqAttrCreateBid = []string{} + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"manage-req-attrs", "--from", s.addr1.String(), "--market", "420", + "--ask-add", "*.my.attr", "--bid-remove", "buyer.kyc"}, + expectedCode: 0, + }, + { + name: "req attrs put back", + preRun: func() ([]string, func(*sdk.TxResponse)) { + market420 := s.getMarket("420") + market420.ReqAttrCreateAsk = []string{"seller.kyc"} + market420.ReqAttrCreateBid = []string{"buyer.kyc"} + return nil, s.getMarketFollowup("420", market420) + }, + args: []string{"manage-market-req-attrs", "--from", s.addr1.String(), "--market", "420", + "--ask-remove", "*.my.attr", "--bid-add", "buyer.kyc"}, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxGovCreateMarket() { + tests := []txCmdTestCase{ + { + name: "cmd error", + args: []string{"gov-create-market", "--from", s.addr1.String(), "--create-ask", "bananas"}, + expInErr: []string{"invalid coin expression: \"bananas\""}, + }, + { + name: "wrong authority", + args: []string{"create-market", "--from", s.addr2.String(), "--authority", s.addr2.String(), "--name", "Whatever"}, + expInRawLog: []string{"failed to execute message", + s.addr2.String(), "expected gov account as only signer for proposal message", + }, + expectedCode: invSigCode, + }, + { + name: "prop created", + preRun: func() ([]string, func(*sdk.TxResponse)) { + expMsg := &exchange.MsgGovCreateMarketRequest{ + Authority: cli.AuthorityAddr.String(), + Market: exchange.Market{ + MarketId: 0, + MarketDetails: exchange.MarketDetails{ + Name: "My New Market", + Description: "Market 01E6", + }, + FeeCreateAskFlat: sdk.NewCoins(sdk.NewInt64Coin("acorn", 100)), + FeeCreateBidFlat: sdk.NewCoins(sdk.NewInt64Coin("acorn", 110)), + AcceptingOrders: true, + AllowUserSettlement: false, + AccessGrants: []exchange.AccessGrant{ + {Address: s.addr4.String(), Permissions: exchange.AllPermissions()}, + }, + }, + } + return nil, s.govPropFollowup(expMsg) + }, + args: []string{"create-market", "--from", s.addr4.String(), + "--name", "My New Market", + "--description", "Market 01E6", + "--create-ask", "100acorn", "--create-bid", "110acorn", + "--accepting-orders", "--access-grants", s.addr4.String() + ":all", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxGovManageFees() { + tests := []txCmdTestCase{ + { + name: "cmd error", + args: []string{"gov-manage-fees", "--from", s.addr1.String(), "--ask-add", "bananas", "--market", "12"}, + expInErr: []string{"invalid coin expression: \"bananas\""}, + }, + { + name: "wrong authority", + args: []string{"manage-fees", "--from", s.addr2.String(), "--authority", s.addr2.String(), + "--ask-add", "99banana", "--market", "12"}, + expInRawLog: []string{"failed to execute message", + s.addr2.String(), "expected gov account as only signer for proposal message", + }, + expectedCode: invSigCode, + }, + { + name: "prop created", + preRun: func() ([]string, func(*sdk.TxResponse)) { + expMsg := &exchange.MsgGovManageFeesRequest{ + Authority: cli.AuthorityAddr.String(), + MarketId: 419, + AddFeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("banana", 99)}, + RemoveFeeSellerSettlementFlat: []sdk.Coin{sdk.NewInt64Coin("acorn", 12)}, + AddFeeBuyerSettlementRatios: []exchange.FeeRatio{ + {Price: sdk.NewInt64Coin("plum", 100), Fee: sdk.NewInt64Coin("plum", 1)}, + }, + } + return nil, s.govPropFollowup(expMsg) + }, + args: []string{"update-fees", "--from", s.addr4.String(), "--market", "419", + "--ask-add", "99banana", "--seller-flat-remove", "12acorn", + "--buyer-ratios-add", "100plum:1plum", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} + +func (s *CmdTestSuite) TestCmdTxGovUpdateParams() { + tests := []txCmdTestCase{ + { + name: "cmd error", + args: []string{"gov-update-params", "--from", s.addr1.String(), "--default", "500", "--split", "eight"}, + expInErr: []string{"invalid denom split \"eight\": expected format :"}, + }, + { + name: "wrong authority", + args: []string{"gov-params", "--from", s.addr2.String(), "--authority", s.addr2.String(), "--default", "500"}, + expInRawLog: []string{"failed to execute message", + s.addr2.String(), "expected gov account as only signer for proposal message", + }, + expectedCode: invSigCode, + }, + { + name: "prop created", + preRun: func() ([]string, func(*sdk.TxResponse)) { + expMsg := &exchange.MsgGovUpdateParamsRequest{ + Authority: cli.AuthorityAddr.String(), + Params: exchange.Params{ + DefaultSplit: 777, + DenomSplits: []exchange.DenomSplit{ + {Denom: "apple", Split: 500}, + {Denom: "acorn", Split: 555}, + }, + }, + } + return nil, s.govPropFollowup(expMsg) + }, + args: []string{"params", "--from", s.addr4.String(), + "--default", "777", "--split", "apple:500", "--split", "acorn:555", + }, + expectedCode: 0, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + s.runTxCmdTestCase(tc) + }) + } +} diff --git a/x/exchange/fulfillment_test.go b/x/exchange/fulfillment_test.go index 77dd91ad28..4229caf3bb 100644 --- a/x/exchange/fulfillment_test.go +++ b/x/exchange/fulfillment_test.go @@ -6399,7 +6399,7 @@ func TestFilterOrders(t *testing.T) { func TestGetNAVs(t *testing.T) { coin := func(coinStr string) sdk.Coin { - rv, err := parseCoin(coinStr) + rv, err := ParseCoin(coinStr) require.NoError(t, err, "parseCoin(%q)", coinStr) return rv } diff --git a/x/exchange/market.go b/x/exchange/market.go index 2ac61d3e4b..b2accb2a52 100644 --- a/x/exchange/market.go +++ b/x/exchange/market.go @@ -198,11 +198,12 @@ func ValidateBuyerFeeRatios(ratios []FeeRatio) error { return errors.Join(errs...) } -// parseCoin parses a string into an sdk.Coin -func parseCoin(coinStr string) (sdk.Coin, error) { - // The sdk.ParseCoinNormalized allows for decimals and just truncates if there are some. +// ParseCoin parses a string into an sdk.Coin +func ParseCoin(coinStr string) (sdk.Coin, error) { + // The sdk.ParseCoinNormalized func allows for decimals and just truncates if there are some. // But I want an error if there's a decimal portion. - // It's errors also always have "invalid decimal coin expression", and I don't want "decimal" in these errors. + // Its errors also always have "invalid decimal coin expression", and I don't want "decimal" in these errors. + // I also like having the offending coin string quoted since it's safer and clearer when coinStr is "". decCoin, err := sdk.ParseDecCoin(coinStr) if err != nil || !decCoin.Amount.IsInteger() { return sdk.Coin{}, fmt.Errorf("invalid coin expression: %q", coinStr) @@ -217,11 +218,11 @@ func ParseFeeRatio(ratio string) (*FeeRatio, error) { if len(parts) != 2 { return nil, fmt.Errorf("cannot create FeeRatio from %q: expected exactly one colon", ratio) } - price, err := parseCoin(parts[0]) + price, err := ParseCoin(parts[0]) if err != nil { return nil, fmt.Errorf("cannot create FeeRatio from %q: price: %w", ratio, err) } - fee, err := parseCoin(parts[1]) + fee, err := ParseCoin(parts[1]) if err != nil { return nil, fmt.Errorf("cannot create FeeRatio from %q: fee: %w", ratio, err) } diff --git a/x/exchange/market_test.go b/x/exchange/market_test.go index 1e878bb46a..966a2d9496 100644 --- a/x/exchange/market_test.go +++ b/x/exchange/market_test.go @@ -722,6 +722,67 @@ func TestValidateBuyerFeeRatios(t *testing.T) { } } +func TestParseCoin(t *testing.T) { + tests := []struct { + name string + coinStr string + expCoin sdk.Coin + expErr bool + }{ + { + name: "empty string", + coinStr: "", + expErr: true, + }, + { + name: "no denom", + coinStr: "12345", + expErr: true, + }, + { + name: "no amount", + coinStr: "nhash", + expErr: true, + }, + { + name: "decimal amount", + coinStr: "123.45banana", + expErr: true, + }, + { + name: "zero amount", + coinStr: "0apple", + expCoin: sdk.NewInt64Coin("apple", 0), + }, + { + name: "normal", + coinStr: "500acorn", + expCoin: sdk.NewInt64Coin("acorn", 500), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var expErr string + if tc.expErr { + expErr = fmt.Sprintf("invalid coin expression: %q", tc.coinStr) + } + + var coin sdk.Coin + var err error + testFunc := func() { + coin, err = ParseCoin(tc.coinStr) + } + require.NotPanics(t, testFunc, "ParseCoin(%q)", tc.coinStr) + assertions.AssertErrorValue(t, err, expErr, "ParseCoin(%q) error", tc.coinStr) + if !assert.Equal(t, tc.expCoin, coin, "ParseCoin(%q) coin", tc.coinStr) { + t.Logf("Expected: %s", tc.expCoin) + t.Logf(" Actual: %s", coin) + } + }) + } +} + func TestParseFeeRatio(t *testing.T) { ratioStr := func(ratio *FeeRatio) string { if ratio == nil {