From 31b8a8734530d3952c561f2ea8e55a64007ae433 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Tue, 21 Nov 2023 11:43:25 -0700 Subject: [PATCH] Exchange CLI commands (#1732) * [1701]: Create root tx and query commands and stub out all the sub-commands. * [1701]: Add some standard flags to the commands. * [1701]: Do the create-ask and create-bid commands. * [1701]: Create the cancel-order command. * [1701]: Commands for fill-asks and fill-bids. * [1701]: Create the market-settle command. Add the ability to provide a buyer, seller, or signer other than the --from to the other commands. * [1701]: Reorder the stuff in helpers.go to be alphabetized by the flag. * [1701]: Rename helpers.go to flags.go so I can extract the cmd flag adds/reads into helpers.go and leave flags.go as the generic more flag stuff. * [1701]: Refactor a few flag funcs to take in the client context as the first arg since you're supposed to always have the context first (even though it's a secondary thing in these cases, but oh well). * [1701]: Extract the flag additions and msg creation into helper functions and use those in the commands instead of having them directly in the commands. This should make testing easier since those parts can then be tested without needing to run a command (and fire up a chain). * [1701]: Create the market-withdraw command. Rename the helpers for consitency. * [1701]: Create the market-set-external-id command. * [1701]: Change the comments on the flags adders and MakeMsgs to reference eachother instead of the command. * [1701]: Create the market-details command. * [1701]: Create the market-enabled and market-user-settle commands. * [1701]: Create the market-permissions command. * [1701]: Create the market-req-attrs command. * [1701]: Make ParseCoin public. * [1702]: Create the create-market command. * [1701]: Get rid of all the simple AddFlagX and ReadFlagX funcs and just put that stuff in the AddFlagsMsgX and MakeMsgX funcs. Swap most of the stuff in helpers.go and flags.go. * [1701]: Use the new MarkFlagsOneRequired func for enable/disable. * [1701]: Add tx flags and gov flags before the msg-specific ones. Change MarkFlagRequired into MarkFlagsRequired and take in varargs names. When adding flags, mark all requireds together and add some one-of lists. * [1701]: Change the create-market command to gov-create-market. Ignore the Long field and just put everything in the Use field because that reads much better in output. Add all the flags to the Use line in the funcs that add the flags. Also add the description sections to the Use field in there too (that stuff used to be in the Long field). Don't include [flags] at the end of the Use output. Don't include help if there's an error generating/sending the tx. * [1701]: Create the gov-manage-fees command. * [1701]: Create the gov-update-params command. * [1701]: Some func reorg in helpers.go and some func renames for standardization. * [1701]: Create the order-fee-calc query command. * [1701]: Create the get-order and get-order-by-external-id query commands. * [1701]: Create the market-orders, owner-orders, and asset-orders queries. Move Args definition into the flag setters for queries. * [1701]: Move the Args definition into the flag adders for the tx commands too. * [1701]: Create the all-orders query command. * [1701]: Create the market and all-markets query commands. * [1701]: Split out the command setup funcs into query_setup.go and tx_setup.go. Reorg the stuff in flags.go and helpers.go to hopefully make more sense. * [1701]: Rename the cmd setup funcs from AddFlags to SetupCmd to better reflect what they're doing. * [1701]: Move the tx and query flag additions back into the command funcs. * [1701]: Create the params query. * [1701]: Create the validate-create-market, validate-market, and validate-manage-fees queries. * [1701]: Stub out a bunch of TODOs for unit tests and create the CmdTestSuite struct and setup. * [1701]: Some tweaks to the flags stuff to clean up error messages. Write unit tests on all the flags stuff. * [1701]: Fix AddQueryExample to not add an ending space if no args are given. Unit tests on the helpers. * [1701]: Unit tests on all the cmd setup funcs. * [1701]: Fix a couple of the query makers. Unit tests on all the query makers. * [1701]: Unit tests on the tx makers. * [1701]: Create the beginnings of the Cmd tests. * [1701]: Create ParseCoins to replace sdk.ParseCoinsNormalized similar to ParseCoin. * [1701]: Add some orders to the cli unit test gen state and write some followup helpers to run some checks. * [1701]: Lint fixes. * [1701]: Unit tests on the order-fee-calc and get-order commands. * [1701]: Unit tests on the order getter commands. * [1701]: Unit tests on the market and all-markets commands. * [1701]: Unit tests on the params query. * [1701]: Unit tests on the validation queries. Query unit tests done. * [1701]: Unit tests on the create-ask and create-bid endpoints. * [1701]: Unit tests on the cancel-order command. * [1701]: Unit tests on the fill-bids, fill-asks, and market-settle endpoints. * [1701]: Unit tests on the market-set-external-id command. * [1701]: Unit tests on the market-withdraw, market-update-details, market-update-enabled, and market-update-user-settle commands. * [1701]: Unit tests on the market-permissions, and market-req-attrs commands. * [1701]: Unit tests on the gov prop commands. * [1701]: A light reorg in cli_test.go and a couple lint/compilation fixes. * [1701]: Wrap all the no xxx provided errors in angle-brackets. * [1701]: Update ReqAdminDesc to have angle-brackets around stuff. Get rid of the suite's authorityAddr field and just use cli.AuthorityAddr for that. Tweak some comments in helpers.go. * [1701]: Add the get-params alias to the params command. Tweak a few other comments. * [1701]: for the create-market and manage-fees commands and validation queries, allow the proposal to be provided via file. * [1701]: Change ReadFlagBoolOrDefault to pay attention to whether or not the flag was provided rather than trying to just go off the value returned by GetBool. Fix a failing unit test in TestReadFlagAuthority that broke when I changed it to return the default even when there's an error. * [1701]: Create getSingleMsgFromPropFlag and use that in ReadMsgGovManageFeesRequestFromProposalFlag and ReadMsgGovCreateMarketRequestFromProposalFlag. Add some extra comments in flags.go about assumptions and related funcs. * [1701]: Add changelog entry. * [1701]: Add a little reasoning to the comment in ParseCoin. * [1701]: Update ReqSignerDesc to wrap the field name in angle-brackets (like the other stuff does). * [1701]: Tweak a couple helper.go descriptions and comments. * [1701]: Make a function for adding the [--asks|--bids] flags to the queries. * [1701]: Fix some comments in flags.go. * [1701]: Fix the addrs used in TestParseAccessGrants. * [1701]: A little cleanup of cli_test.go. * [1701]: Tweak the aliases on the query commands. * [1701]: Tweak the aliases in the tx commands that update or manage market things. * [1701]: Use --after (instead of --order) for the after_order_id query fields. * [1701]: Fix some typos. * [1701]: Update ReadFlagAuthorityOrDefault to use the standard authority as the default if no default is provided. * [1701]: A little cleanup in query_test.go. * [1701]: Move the MarkFlagsRequired into flags.go (from helpers.go). --- CHANGELOG.md | 4 + x/exchange/client/cli/cli_test.go | 1015 ++++++++ x/exchange/client/cli/flags.go | 693 ++++++ x/exchange/client/cli/flags_test.go | 2719 +++++++++++++++++++++ x/exchange/client/cli/helpers.go | 273 +++ x/exchange/client/cli/helpers_test.go | 355 +++ x/exchange/client/cli/query.go | 219 +- x/exchange/client/cli/query_setup.go | 409 ++++ x/exchange/client/cli/query_setup_test.go | 1271 ++++++++++ x/exchange/client/cli/query_test.go | 547 +++++ x/exchange/client/cli/tx.go | 276 ++- x/exchange/client/cli/tx_setup.go | 743 ++++++ x/exchange/client/cli/tx_setup_test.go | 1668 +++++++++++++ x/exchange/client/cli/tx_test.go | 941 +++++++ x/exchange/fulfillment_test.go | 2 +- x/exchange/market.go | 13 +- x/exchange/market_test.go | 61 + 17 files changed, 11196 insertions(+), 13 deletions(-) create mode 100644 x/exchange/client/cli/cli_test.go create mode 100644 x/exchange/client/cli/flags.go create mode 100644 x/exchange/client/cli/flags_test.go create mode 100644 x/exchange/client/cli/helpers.go create mode 100644 x/exchange/client/cli/helpers_test.go create mode 100644 x/exchange/client/cli/query_setup.go create mode 100644 x/exchange/client/cli/query_setup_test.go create mode 100644 x/exchange/client/cli/query_test.go create mode 100644 x/exchange/client/cli/tx_setup.go create mode 100644 x/exchange/client/cli/tx_setup_test.go create mode 100644 x/exchange/client/cli/tx_test.go 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 {