diff --git a/CHANGELOG.md b/CHANGELOG.md index 273478719204..cad0d2cfd9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +* (x/bank) [#12706](https://github.com/cosmos/cosmos-sdk/pull/12706) Added the `chain-id` flag to the `AddTxFlagsToCmd` API. There is no longer a need to explicitly register this flag on commands whens `AddTxFlagsToCmd` is already called. * [#12791](https://github.com/cosmos/cosmos-sdk/pull/12791) Bump the math library used in the sdk and replace old usages of sdk.* * (x/params) [#12615](https://github.com/cosmos/cosmos-sdk/pull/12615) Add `GetParamSetIfExists` function to params `Subspace` to prevent panics on breaking changes. * [#12717](https://github.com/cosmos/cosmos-sdk/pull/12717) Use injected encoding params in simapp. @@ -70,6 +71,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### API Breaking Changes +* (x/bank) [#12706](https://github.com/cosmos/cosmos-sdk/pull/12706) Removed the `testutil` package from the `x/bank/client` package. * (simapp) [#12747](https://github.com/cosmos/cosmos-sdk/pull/12747) Remove `simapp.MakeTestEncodingConfig`. Please use `moduletestutil.MakeTestEncodingConfig` (`types/module/testutil`) in tests instead. * (x/bank) [#12648](https://github.com/cosmos/cosmos-sdk/pull/12648) `NewSendAuthorization` takes a new argument of an optional list of addresses allowed to receive bank assests via authz MsgSend grant. You can pass `nil` for the same behavior as before, i.e. any recipient is allowed. * (x/bank) [\#12593](https://github.com/cosmos/cosmos-sdk/pull/12593) Add `SpendableCoin` method to `BaseViewKeeper` diff --git a/client/account_retriever.go b/client/account_retriever.go index 8e2fd14c1fcd..24de5423e2ea 100644 --- a/client/account_retriever.go +++ b/client/account_retriever.go @@ -22,3 +22,26 @@ type AccountRetriever interface { EnsureExists(clientCtx Context, addr sdk.AccAddress) error GetAccountNumberSequence(clientCtx Context, addr sdk.AccAddress) (accNum uint64, accSeq uint64, err error) } + +var _ AccountRetriever = (*MockAccountRetriever)(nil) + +// MockAccountRetriever defines a no-op basic AccountRetriever that can be used +// in mocked contexts. Tests or context that need more sophisticated testing +// state should implement their own mock AccountRetriever. +type MockAccountRetriever struct{} + +func (mar MockAccountRetriever) GetAccount(_ Context, _ sdk.AccAddress) (Account, error) { + return nil, nil +} + +func (mar MockAccountRetriever) GetAccountWithHeight(_ Context, _ sdk.AccAddress) (Account, int64, error) { + return nil, 0, nil +} + +func (mar MockAccountRetriever) EnsureExists(_ Context, _ sdk.AccAddress) error { + return nil +} + +func (mar MockAccountRetriever) GetAccountNumberSequence(_ Context, _ sdk.AccAddress) (uint64, uint64, error) { + return 0, 0, nil +} diff --git a/client/context.go b/client/context.go index be5c84af6070..b635ff953d2e 100644 --- a/client/context.go +++ b/client/context.go @@ -7,14 +7,10 @@ import ( "io" "os" + "github.com/gogo/protobuf/proto" "github.com/spf13/viper" - - "sigs.k8s.io/yaml" - "google.golang.org/grpc" - - "github.com/gogo/protobuf/proto" - rpcclient "github.com/tendermint/tendermint/rpc/client" + "sigs.k8s.io/yaml" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -26,7 +22,7 @@ import ( // handling and queries. type Context struct { FromAddress sdk.AccAddress - Client rpcclient.Client + Client TendermintRPC GRPCClient *grpc.ClientConn ChainID string Codec codec.Codec @@ -128,7 +124,7 @@ func (ctx Context) WithHeight(height int64) Context { // WithClient returns a copy of the context with an updated RPC client // instance. -func (ctx Context) WithClient(client rpcclient.Client) Context { +func (ctx Context) WithClient(client TendermintRPC) Context { ctx.Client = client return ctx } diff --git a/client/flags/flags.go b/client/flags/flags.go index 12bd7fd24cf4..9164ab599d6a 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -79,6 +79,7 @@ const ( FlagReverse = "reverse" FlagTip = "tip" FlagAux = "aux" + FlagOutput = tmcli.OutputFlag // Tendermint logging flags FlagLogLevel = "log_level" @@ -93,7 +94,7 @@ var LineBreak = &cobra.Command{Run: func(*cobra.Command, []string) {}} func AddQueryFlagsToCmd(cmd *cobra.Command) { cmd.Flags().String(FlagNode, "tcp://localhost:26657", ": to Tendermint RPC interface for this chain") cmd.Flags().Int64(FlagHeight, 0, "Use a specific height to query state at (this can error if the node is pruning state)") - cmd.Flags().StringP(tmcli.OutputFlag, "o", "text", "Output format (text|json)") + cmd.Flags().StringP(FlagOutput, "o", "text", "Output format (text|json)") // some base commands does not require chainID e.g `simd testnet` while subcommands do // hence the flag should not be required for those commands @@ -102,7 +103,7 @@ func AddQueryFlagsToCmd(cmd *cobra.Command) { // AddTxFlagsToCmd adds common flags to a module tx command. func AddTxFlagsToCmd(cmd *cobra.Command) { - cmd.Flags().StringP(tmcli.OutputFlag, "o", "json", "Output format (text|json)") + cmd.Flags().StringP(FlagOutput, "o", "json", "Output format (text|json)") cmd.Flags().String(FlagKeyringDir, "", "The client Keyring directory; if omitted, the default 'home' directory will be used") cmd.Flags().String(FlagFrom, "", "Name or address of private key with which to sign") cmd.Flags().Uint64P(FlagAccountNumber, "a", 0, "The account number of the signing account (offline mode only)") @@ -125,6 +126,7 @@ func AddTxFlagsToCmd(cmd *cobra.Command) { cmd.Flags().String(FlagFeeGranter, "", "Fee granter grants fees for the transaction") cmd.Flags().String(FlagTip, "", "Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator") cmd.Flags().Bool(FlagAux, false, "Generate aux signer data instead of sending a tx") + cmd.Flags().String(FlagChainID, "", "The network chain ID") // --gas can accept integers and "auto" cmd.Flags().String(FlagGas, "", fmt.Sprintf("gas limit to set per-transaction; set to %q to calculate sufficient gas automatically (default %d)", GasFlagAuto, DefaultGasLimit)) diff --git a/client/grpc_query.go b/client/grpc_query.go index a5b348ca1058..da8b2e8fa268 100644 --- a/client/grpc_query.go +++ b/client/grpc_query.go @@ -7,10 +7,11 @@ import ( "reflect" "strconv" - "github.com/cosmos/cosmos-sdk/codec" proto "github.com/gogo/protobuf/proto" "google.golang.org/grpc/encoding" + "github.com/cosmos/cosmos-sdk/codec" + gogogrpc "github.com/gogo/protobuf/grpc" abci "github.com/tendermint/tendermint/abci/types" "google.golang.org/grpc" diff --git a/client/query.go b/client/query.go index f81787147e91..88f0595c5ad3 100644 --- a/client/query.go +++ b/client/query.go @@ -6,12 +6,11 @@ import ( "strings" "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - abci "github.com/tendermint/tendermint/abci/types" tmbytes "github.com/tendermint/tendermint/libs/bytes" rpcclient "github.com/tendermint/tendermint/rpc/client" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/cosmos/cosmos-sdk/store/rootmulti" sdk "github.com/cosmos/cosmos-sdk/types" @@ -20,7 +19,7 @@ import ( // GetNode returns an RPC client. If the context's client is not defined, an // error is returned. -func (ctx Context) GetNode() (rpcclient.Client, error) { +func (ctx Context) GetNode() (TendermintRPC, error) { if ctx.Client == nil { return nil, errors.New("no RPC client is defined in offline mode") } diff --git a/client/tendermint.go b/client/tendermint.go new file mode 100644 index 000000000000..4eb56f5f2670 --- /dev/null +++ b/client/tendermint.go @@ -0,0 +1,28 @@ +package client + +import ( + "context" + + "github.com/tendermint/tendermint/libs/bytes" + rpcclient "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/coretypes" +) + +// TendermintRPC defines the interface of a Tendermint RPC client needed for +// queries and transaction handling. +type TendermintRPC interface { + rpcclient.ABCIClient + + Validators(ctx context.Context, height *int64, page, perPage *int) (*coretypes.ResultValidators, error) + Status(context.Context) (*coretypes.ResultStatus, error) + Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) + BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) + Tx(ctx context.Context, hash bytes.HexBytes, prove bool) (*coretypes.ResultTx, error) + TxSearch( + ctx context.Context, + query string, + prove bool, + page, perPage *int, + orderBy string, + ) (*coretypes.ResultTxSearch, error) +} diff --git a/client/tx/factory.go b/client/tx/factory.go index 15304e8e5e70..ff8eaec07bdd 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -392,7 +392,6 @@ func (f Factory) getSimPK() (cryptotypes.PubKey, error) { // the updated fields will be returned. func (f Factory) Prepare(clientCtx client.Context) (Factory, error) { fc := f - from := clientCtx.GetFromAddress() if err := fc.accountRetriever.EnsureExists(clientCtx, from); err != nil { diff --git a/server/cmd/execute.go b/server/cmd/execute.go index 4f715d3cc3de..70068ff6b4e4 100644 --- a/server/cmd/execute.go +++ b/server/cmd/execute.go @@ -20,14 +20,11 @@ import ( func Execute(rootCmd *cobra.Command, envPrefix string, defaultHome string) error { // Create and set a client.Context on the command's Context. During the pre-run // of the root command, a default initialized client.Context is provided to - // seed child command execution with values such as AccountRetriver, Keyring, + // seed child command execution with values such as AccountRetriever, Keyring, // and a Tendermint RPC. This requires the use of a pointer reference when // getting and setting the client.Context. Ideally, we utilize // https://github.com/spf13/cobra/pull/1118. - srvCtx := server.NewDefaultContext() - ctx := context.Background() - ctx = context.WithValue(ctx, client.ClientContextKey, &client.Context{}) - ctx = context.WithValue(ctx, server.ServerContextKey, srvCtx) + ctx := CreateExecuteContext(context.Background()) rootCmd.PersistentFlags().String(flags.FlagLogLevel, zerolog.InfoLevel.String(), "The logging level (trace|debug|info|warn|error|fatal|panic)") rootCmd.PersistentFlags().String(flags.FlagLogFormat, tmlog.LogFormatPlain, "The logging format (json|plain)") @@ -35,3 +32,13 @@ func Execute(rootCmd *cobra.Command, envPrefix string, defaultHome string) error executor := tmcli.PrepareBaseCmd(rootCmd, envPrefix, defaultHome) return executor.ExecuteContext(ctx) } + +// CreateExecuteContext returns a base Context with server and client context +// values initialized. +func CreateExecuteContext(ctx context.Context) context.Context { + srvCtx := server.NewDefaultContext() + ctx = context.WithValue(ctx, client.ClientContextKey, &client.Context{}) + ctx = context.WithValue(ctx, server.ServerContextKey, srvCtx) + + return ctx +} diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index 7eb91d5d1c59..88a3b6690ed0 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -216,7 +216,6 @@ func queryCommand() *cobra.Command { ) simapp.ModuleBasics.AddQueryCommands(cmd) - cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") return cmd } @@ -243,7 +242,6 @@ func txCommand() *cobra.Command { ) simapp.ModuleBasics.AddTxCommands(cmd) - cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") return cmd } diff --git a/x/auth/client/cli/tips.go b/x/auth/client/cli/tips.go index 773ad6d619f7..8f86e70b35f9 100644 --- a/x/auth/client/cli/tips.go +++ b/x/auth/client/cli/tips.go @@ -76,7 +76,7 @@ func GetAuxToFeeCommand() *cobra.Command { } flags.AddTxFlagsToCmd(cmd) - cmd.Flags().String(flags.FlagChainID, "", "network chain ID") + return cmd } diff --git a/x/auth/client/cli/tx_multisign.go b/x/auth/client/cli/tx_multisign.go index 48c819cb8eab..0a2824ffe6d0 100644 --- a/x/auth/client/cli/tx_multisign.go +++ b/x/auth/client/cli/tx_multisign.go @@ -65,7 +65,6 @@ The SIGN_MODE_DIRECT sign mode is not supported.' cmd.Flags().String(flags.FlagOutputDocument, "", "The document is written to the given file instead of STDOUT") cmd.Flags().Bool(flagAmino, false, "Generate Amino-encoded JSON suitable for submitting to the txs REST endpoint") flags.AddTxFlagsToCmd(cmd) - cmd.Flags().String(flags.FlagChainID, "", "network chain ID") return cmd } diff --git a/x/auth/client/cli/tx_sign.go b/x/auth/client/cli/tx_sign.go index 53fa016e6ac2..45fd71fa083c 100644 --- a/x/auth/client/cli/tx_sign.go +++ b/x/auth/client/cli/tx_sign.go @@ -54,7 +54,6 @@ account key. It implies --signature-only. cmd.Flags().String(flagMultisig, "", "Address or key name of the multisig account on behalf of which the transaction shall be signed") cmd.Flags().String(flags.FlagOutputDocument, "", "The document will be written to the given file instead of STDOUT") cmd.Flags().Bool(flagSigOnly, true, "Print only the generated signature, then exit") - cmd.Flags().String(flags.FlagChainID, "", "network chain ID") flags.AddTxFlagsToCmd(cmd) cmd.MarkFlagRequired(flags.FlagFrom) @@ -192,7 +191,6 @@ be generated via the 'multisign' command. cmd.Flags().Bool(flagOverwrite, false, "Overwrite existing signatures with a new one. If disabled, new signature will be appended") cmd.Flags().Bool(flagSigOnly, false, "Print only the signatures") cmd.Flags().String(flags.FlagOutputDocument, "", "The document will be written to the given file instead of STDOUT") - cmd.Flags().String(flags.FlagChainID, "", "The network chain ID") cmd.Flags().Bool(flagAmino, false, "Generate Amino encoded JSON suitable for submiting to the txs REST endpoint") flags.AddTxFlagsToCmd(cmd) diff --git a/x/auth/client/cli/validate_sigs.go b/x/auth/client/cli/validate_sigs.go index 7d4da92bac39..0ad65c71e35a 100644 --- a/x/auth/client/cli/validate_sigs.go +++ b/x/auth/client/cli/validate_sigs.go @@ -30,7 +30,6 @@ transaction will be not be performed as that will require RPC communication with Args: cobra.ExactArgs(1), } - cmd.Flags().String(flags.FlagChainID, "", "The network chain ID") flags.AddTxFlagsToCmd(cmd) return cmd diff --git a/x/bank/client/cli/query.go b/x/bank/client/cli/query.go index 10e5885f9653..df544fd315d6 100644 --- a/x/bank/client/cli/query.go +++ b/x/bank/client/cli/query.go @@ -59,6 +59,7 @@ Example: if err != nil { return err } + denom, err := cmd.Flags().GetString(FlagDenom) if err != nil { return err @@ -75,17 +76,22 @@ Example: if err != nil { return err } + ctx := cmd.Context() + if denom == "" { params := types.NewQueryAllBalancesRequest(addr, pageReq) + res, err := queryClient.AllBalances(ctx, params) if err != nil { return err } + return clientCtx.PrintProto(res) } params := types.NewQueryBalanceRequest(addr, denom) + res, err := queryClient.Balance(ctx, params) if err != nil { return err @@ -125,6 +131,7 @@ To query for the client metadata of a specific coin denomination use: if err != nil { return err } + denom, err := cmd.Flags().GetString(FlagDenom) if err != nil { return err @@ -178,6 +185,7 @@ To query for the total supply of a specific coin denomination use: if err != nil { return err } + denom, err := cmd.Flags().GetString(FlagDenom) if err != nil { return err @@ -190,6 +198,7 @@ To query for the total supply of a specific coin denomination use: if err != nil { return err } + if denom == "" { res, err := queryClient.TotalSupply(ctx, &types.QueryTotalSupplyRequest{Pagination: pageReq}) if err != nil { diff --git a/x/bank/client/cli/query_test.go b/x/bank/client/cli/query_test.go new file mode 100644 index 000000000000..b782f0799ce8 --- /dev/null +++ b/x/bank/client/cli/query_test.go @@ -0,0 +1,362 @@ +package cli_test + +import ( + "bytes" + "context" + "fmt" + "io" + + "github.com/gogo/protobuf/proto" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func (s *CLITestSuite) TestGetBalancesCmd() { + accounts := s.createKeyringAccounts(1) + + cmd := cli.GetBalancesCmd() + cmd.SetOutput(io.Discard) + + testCases := []struct { + name string + ctxGen func() client.Context + args []string + expectResult proto.Message + expectErr bool + }{ + { + "valid query", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QueryAllBalancesResponse{}) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + accounts[0].address.String(), + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QueryAllBalancesResponse{}, + false, + }, + { + "valid query with denom", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QueryBalanceResponse{ + Balance: &sdk.Coin{}, + }) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + accounts[0].address.String(), + fmt.Sprintf("--%s=photon", cli.FlagDenom), + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &sdk.Coin{}, + false, + }, + { + "invalid address", + func() client.Context { + return s.baseCtx + }, + []string{ + "foo", + }, + nil, + true, + }, + { + "invalid denom", + func() client.Context { + c := newMockTendermintRPC(abci.ResponseQuery{ + Code: 1, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + accounts[0].address.String(), + fmt.Sprintf("--%s=foo", cli.FlagDenom), + }, + nil, + true, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + var outBuf bytes.Buffer + + clientCtx := tc.ctxGen().WithOutput(&outBuf) + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd.SetContext(ctx) + cmd.SetArgs(tc.args) + + s.Require().NoError(client.SetCmdClientContextHandler(clientCtx, cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(s.encCfg.Codec.UnmarshalJSON(outBuf.Bytes(), tc.expectResult)) + s.Require().NoError(err) + } + }) + } +} + +func (s *CLITestSuite) TestGetCmdDenomsMetadata() { + cmd := cli.GetCmdDenomsMetadata() + cmd.SetOutput(io.Discard) + + testCases := []struct { + name string + ctxGen func() client.Context + args []string + expectResult proto.Message + expectErr bool + }{ + { + "valid query", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QueryDenomsMetadataResponse{}) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QueryDenomsMetadataResponse{}, + false, + }, + { + "valid query with denom", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QueryDenomMetadataResponse{}) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=photon", cli.FlagDenom), + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QueryDenomMetadataResponse{}, + false, + }, + { + "invalid query with denom", + func() client.Context { + c := newMockTendermintRPC(abci.ResponseQuery{ + Code: 1, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=foo", cli.FlagDenom), + }, + nil, + true, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + var outBuf bytes.Buffer + + clientCtx := tc.ctxGen().WithOutput(&outBuf) + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd.SetContext(ctx) + cmd.SetArgs(tc.args) + + s.Require().NoError(client.SetCmdClientContextHandler(clientCtx, cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(s.encCfg.Codec.UnmarshalJSON(outBuf.Bytes(), tc.expectResult)) + s.Require().NoError(err) + } + }) + } +} + +func (s *CLITestSuite) TestGetCmdQueryTotalSupply() { + cmd := cli.GetCmdQueryTotalSupply() + cmd.SetOutput(io.Discard) + + testCases := []struct { + name string + ctxGen func() client.Context + args []string + expectResult proto.Message + expectErr bool + }{ + { + "valid query", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QueryTotalSupplyResponse{}) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QueryTotalSupplyResponse{}, + false, + }, + { + "valid query with denom", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QuerySupplyOfResponse{ + Amount: sdk.Coin{}, + }) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=photon", cli.FlagDenom), + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &sdk.Coin{}, + false, + }, + { + "invalid query with denom", + func() client.Context { + c := newMockTendermintRPC(abci.ResponseQuery{ + Code: 1, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=foo", cli.FlagDenom), + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + nil, + true, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + var outBuf bytes.Buffer + + clientCtx := tc.ctxGen().WithOutput(&outBuf) + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd.SetContext(ctx) + cmd.SetArgs(tc.args) + + s.Require().NoError(client.SetCmdClientContextHandler(clientCtx, cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(s.encCfg.Codec.UnmarshalJSON(outBuf.Bytes(), tc.expectResult)) + s.Require().NoError(err) + } + }) + } +} + +func (s *CLITestSuite) TestGetCmdQuerySendEnabled() { + cmd := cli.GetCmdQuerySendEnabled() + cmd.SetOutput(io.Discard) + + testCases := []struct { + name string + ctxGen func() client.Context + args []string + expectResult proto.Message + expectErr bool + }{ + { + "valid query", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QuerySendEnabledResponse{ + SendEnabled: []*types.SendEnabled{}, + }) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QuerySendEnabledResponse{}, + false, + }, + { + "valid query with denoms", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&types.QuerySendEnabledResponse{ + SendEnabled: []*types.SendEnabled{}, + }) + c := newMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + []string{ + "photon", + "stake", + fmt.Sprintf("--%s=json", flags.FlagOutput), + }, + &types.QuerySendEnabledResponse{}, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + var outBuf bytes.Buffer + + clientCtx := tc.ctxGen().WithOutput(&outBuf) + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd.SetContext(ctx) + cmd.SetArgs(tc.args) + + s.Require().NoError(client.SetCmdClientContextHandler(clientCtx, cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(s.encCfg.Codec.UnmarshalJSON(outBuf.Bytes(), tc.expectResult)) + s.Require().NoError(err) + } + }) + } +} diff --git a/x/bank/client/cli/suite_test.go b/x/bank/client/cli/suite_test.go new file mode 100644 index 000000000000..82fcd699befc --- /dev/null +++ b/x/bank/client/cli/suite_test.go @@ -0,0 +1,96 @@ +package cli_test + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/bytes" + rpcclient "github.com/tendermint/tendermint/rpc/client" + rpcclientmock "github.com/tendermint/tendermint/rpc/client/mock" + "github.com/tendermint/tendermint/rpc/coretypes" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + testutilmod "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +var _ client.TendermintRPC = (*mockTendermintRPC)(nil) + +type mockTendermintRPC struct { + rpcclientmock.Client + + responseQuery abci.ResponseQuery +} + +func newMockTendermintRPC(respQuery abci.ResponseQuery) mockTendermintRPC { + return mockTendermintRPC{responseQuery: respQuery} +} + +func (_ mockTendermintRPC) BroadcastTxSync(context.Context, tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { + return &coretypes.ResultBroadcastTx{Code: 0}, nil +} + +func (m mockTendermintRPC) ABCIQueryWithOptions( + _ context.Context, + _ string, _ bytes.HexBytes, + _ rpcclient.ABCIQueryOptions, +) (*coretypes.ResultABCIQuery, error) { + return &coretypes.ResultABCIQuery{Response: m.responseQuery}, nil +} + +type account struct { + name string + address sdk.AccAddress +} + +type CLITestSuite struct { + suite.Suite + + kr keyring.Keyring + encCfg testutilmod.TestEncodingConfig + baseCtx client.Context +} + +func TestMigrateTestSuite(t *testing.T) { + suite.Run(t, new(CLITestSuite)) +} + +func (s *CLITestSuite) SetupSuite() { + s.encCfg = testutilmod.MakeTestEncodingConfig(bank.AppModuleBasic{}) + s.kr = keyring.NewInMemory(s.encCfg.Codec) + s.baseCtx = client.Context{}. + WithKeyring(s.kr). + WithTxConfig(s.encCfg.TxConfig). + WithCodec(s.encCfg.Codec). + WithClient(mockTendermintRPC{Client: rpcclientmock.New()}). + WithAccountRetriever(client.MockAccountRetriever{}). + WithOutput(io.Discard) +} + +func (s *CLITestSuite) createKeyringAccounts(num int) []account { + accounts := make([]account, num) + for i := range accounts { + record, _, err := s.kr.NewMnemonic( + fmt.Sprintf("key-%d", i), + keyring.English, + sdk.FullFundraiserPath, + keyring.DefaultBIP39Passphrase, + hd.Secp256k1) + s.Require().NoError(err) + + addr, err := record.GetAddress() + s.Require().NoError(err) + + accounts[i] = account{name: record.Name, address: addr} + } + + return accounts +} diff --git a/x/bank/client/cli/tx.go b/x/bank/client/cli/tx.go index 5deb617b8952..b0bac9b8282d 100644 --- a/x/bank/client/cli/tx.go +++ b/x/bank/client/cli/tx.go @@ -138,7 +138,6 @@ When using '--dry-run' a key name cannot be used, only a bech32 address. } cmd.Flags().Bool(FlagSplit, false, "Send the equally split token amount to each address") - flags.AddTxFlagsToCmd(cmd) return cmd diff --git a/x/bank/client/cli/tx_test.go b/x/bank/client/cli/tx_test.go new file mode 100644 index 000000000000..ba0904f14f8e --- /dev/null +++ b/x/bank/client/cli/tx_test.go @@ -0,0 +1,211 @@ +package cli_test + +import ( + "context" + "fmt" + "io" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/client/cli" +) + +func (s *CLITestSuite) TestSendTxCmd() { + accounts := s.createKeyringAccounts(1) + cmd := cli.NewSendTxCmd() + cmd.SetOutput(io.Discard) + + extraArgs := []string{ + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin("photon", sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=test-chain", flags.FlagChainID), + } + + testCases := []struct { + name string + ctxGen func() client.Context + from, to sdk.AccAddress + amount sdk.Coins + extraArgs []string + expectErr bool + }{ + { + "valid transaction", + func() client.Context { + return s.baseCtx + }, + accounts[0].address, + accounts[0].address, + sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(10)), + sdk.NewCoin("photon", sdk.NewInt(40)), + ), + extraArgs, + false, + }, + { + "invalid to address", + func() client.Context { + return s.baseCtx + }, + accounts[0].address, + sdk.AccAddress{}, + sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(10)), + sdk.NewCoin("photon", sdk.NewInt(40)), + ), + extraArgs, + true, + }, + { + "invalid coins", + func() client.Context { + return s.baseCtx + }, + accounts[0].address, + accounts[0].address, + nil, + extraArgs, + true, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd.SetContext(ctx) + cmd.SetArgs(append([]string{tc.from.String(), tc.to.String(), tc.amount.String()}, tc.extraArgs...)) + + s.Require().NoError(client.SetCmdClientContextHandler(tc.ctxGen(), cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(err) + } + }) + } +} + +func (s *CLITestSuite) TestMultiSendTxCmd() { + accounts := s.createKeyringAccounts(3) + + cmd := cli.NewMultiSendTxCmd() + cmd.SetOutput(io.Discard) + + extraArgs := []string{ + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin("photon", sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=test-chain", flags.FlagChainID), + } + + testCases := []struct { + name string + ctxGen func() client.Context + from string + to []string + amount sdk.Coins + extraArgs []string + expectErr bool + }{ + { + "valid transaction", + func() client.Context { + return s.baseCtx + }, + accounts[0].address.String(), + []string{ + accounts[1].address.String(), + accounts[2].address.String(), + }, + sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(10)), + sdk.NewCoin("photon", sdk.NewInt(40)), + ), + extraArgs, + false, + }, + { + "invalid from address", + func() client.Context { + return s.baseCtx + }, + "foo", + []string{ + accounts[1].address.String(), + accounts[2].address.String(), + }, + sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(10)), + sdk.NewCoin("photon", sdk.NewInt(40)), + ), + extraArgs, + true, + }, + { + "invalid recipients", + func() client.Context { + return s.baseCtx + }, + accounts[0].address.String(), + []string{ + accounts[1].address.String(), + "bar", + }, + sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(10)), + sdk.NewCoin("photon", sdk.NewInt(40)), + ), + extraArgs, + true, + }, + { + "invalid amount", + func() client.Context { + return s.baseCtx + }, + accounts[0].address.String(), + []string{ + accounts[1].address.String(), + accounts[2].address.String(), + }, + nil, + extraArgs, + true, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + ctx := svrcmd.CreateExecuteContext(context.Background()) + + var args []string + args = append(args, tc.from) + args = append(args, tc.to...) + args = append(args, tc.amount.String()) + args = append(args, tc.extraArgs...) + + cmd.SetContext(ctx) + cmd.SetArgs(args) + + s.Require().NoError(client.SetCmdClientContextHandler(tc.ctxGen(), cmd)) + + err := cmd.Execute() + if tc.expectErr { + s.Require().Error(err) + } else { + s.Require().NoError(err) + } + }) + } +} diff --git a/x/bank/client/testutil/cli_helpers.go b/x/bank/client/testutil/cli_helpers.go deleted file mode 100644 index 110b2e6a7979..000000000000 --- a/x/bank/client/testutil/cli_helpers.go +++ /dev/null @@ -1 +0,0 @@ -package testutil diff --git a/x/genutil/client/cli/gentx.go b/x/genutil/client/cli/gentx.go index 4880fa375503..433fa987ea16 100644 --- a/x/genutil/client/cli/gentx.go +++ b/x/genutil/client/cli/gentx.go @@ -209,7 +209,6 @@ $ %s gentx my-key-name 1000000stake --home=/path/to/home/dir --keyring-backend=o cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") cmd.Flags().String(flags.FlagOutputDocument, "", "Write the genesis transaction JSON document to the given file instead of the default location") - cmd.Flags().String(flags.FlagChainID, "", "The network chain ID") cmd.Flags().AddFlagSet(fsCreateValidator) flags.AddTxFlagsToCmd(cmd)