From 6be9d1111a07120e6e76671858e8bcf1a577e63d Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 16:19:00 -0700 Subject: [PATCH 1/8] Create FlagAuthority with all the others since it's going to so commonly be needed by anything with gov props. --- client/flags/flags.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/flags/flags.go b/client/flags/flags.go index 6dacc237374a..922b7a40ebdc 100644 --- a/client/flags/flags.go +++ b/client/flags/flags.go @@ -81,6 +81,7 @@ const ( FlagReverse = "reverse" FlagTip = "tip" FlagAux = "aux" + FlagAuthority = "authority" // Tendermint logging flags FlagLogLevel = "log_level" From 6d092f0d8b0c6f2d84eca2c6ccacc8f278e5264c Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 16:27:11 -0700 Subject: [PATCH 2/8] Create a GenerateOrBroadcastTxCLIAsGovProp function in the governance cli utils that converts the messages to Anys gets the other proposal info from flags and submits the resulting proposal. --- x/gov/client/cli/util.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/x/gov/client/cli/util.go b/x/gov/client/cli/util.go index de44dc5a78dc..830d4871bbfd 100644 --- a/x/gov/client/cli/util.go +++ b/x/gov/client/cli/util.go @@ -9,7 +9,9 @@ import ( "github.com/spf13/pflag" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" govutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" @@ -148,3 +150,31 @@ func ReadGovPropFlags(clientCtx client.Context, flagSet *pflag.FlagSet) (*govv1. return rv, nil } + +// GenerateOrBroadcastTxCLIAsGovProp wraps the provided msgs in a governance proposal +// and calls GenerateOrBroadcastTxCLI for that proposal. +// At least one msg is required. +// This uses flags added by AddGovPropFlagsToCmd to fill in the rest of the proposal. +func GenerateOrBroadcastTxCLIAsGovProp(clientCtx client.Context, flagSet *pflag.FlagSet, msgs ...sdk.Msg) error { + if len(msgs) == 0 { + return fmt.Errorf("no messages to submit") + } + + prop, err := ReadGovPropFlags(clientCtx, flagSet) + if err != nil { + return err + } + + prop.Messages = make([]*codectypes.Any, len(msgs)) + for i, msg := range msgs { + prop.Messages[i], err = codectypes.NewAnyWithValue(msg) + if err != nil { + if len(msgs) == 1 { + return fmt.Errorf("could not wrap %T message as Any: %w", msg, err) + } + return fmt.Errorf("could not wrap message %d (%T) as Any: %w", i, msg, err) + } + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, flagSet, prop) +} From 3909470ea7acd764e494d981b719b3c3447f07f1 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 16:27:55 -0700 Subject: [PATCH 3/8] Create the tx commands that submit proposals. --- x/sanction/client/cli/tx.go | 207 ++++++++++++++++++++++++++++++++++++ x/sanction/module/module.go | 2 +- 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 x/sanction/client/cli/tx.go diff --git a/x/sanction/client/cli/tx.go b/x/sanction/client/cli/tx.go new file mode 100644 index 000000000000..fda535fb2bdb --- /dev/null +++ b/x/sanction/client/cli/tx.go @@ -0,0 +1,207 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "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/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" + "github.com/cosmos/cosmos-sdk/x/sanction" +) + +var ( + // DefaultAuthorityAddr is the default authority to provide in the sanction module's governance proposal messages. + // It should match the value provided to the sanction keeper constructor. + // It is defined as a sdk.AccAddress to be independent of global bech32 HRP definition. + DefaultAuthorityAddr = authtypes.NewModuleAddress(govtypes.ModuleName) + + // exampleTxCmdBase is the base command that gets a user to one of the tx commands in here. + exampleTxCmdBase = fmt.Sprintf("%s tx %s", version.AppName, sanction.ModuleName) + // exampleTxAddr1 is a constant address for use in example strings. + exampleTxAddr1 = sdk.AccAddress("exampleTxAddr1______") + // exampleTxAddr2 is a constant address for use in example strings. + exampleTxAddr2 = sdk.AccAddress("exampleTxAddr2______") +) + +// TxCmd returns the command with sub-commands for specific sanction module Tx interaction. +func TxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: sanction.ModuleName, + Short: "Sanction transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + txCmd.AddCommand( + TxSanctionCmd(), + TxUnsanctionCmd(), + TxUpdateParamsCmd(), + ) + + return txCmd +} + +// TxSanctionCmd returns the command for submitting a MsgSanction governance proposal tx. +func TxSanctionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "sanction
[
...]", + Short: "Submit a governance proposal to sanction one or more addresses", + Long: `Submit a governance proposal to sanction one or more addresses. +At least one address is required; any number of addresses can be provided. +Each address should be a valid bech32 encoded string.`, + Example: fmt.Sprintf(` +$ %[1]s sanction %[2]s +$ %[1]s sanction %[3]s %[2]s +`, + exampleTxCmdBase, exampleTxAddr1, exampleTxAddr2), + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + flagSet := cmd.Flags() + + msgSanction := &sanction.MsgSanction{ + Addresses: args, + Authority: getAuthority(flagSet), + } + if err = msgSanction.ValidateBasic(); err != nil { + return err + } + + return govcli.GenerateOrBroadcastTxCLIAsGovProp(clientCtx, flagSet, msgSanction) + }, + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + addAuthorityFlagToCmd(cmd) + + return cmd +} + +// TxUnsanctionCmd returns the command for submitting a MsgUnsanction governance proposal tx. +func TxUnsanctionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "unsanction
[
...]", + Short: "Submit a governance proposal to unsanction one or more addresses", + Long: `Submit a governance proposal to unsanction one or more addresses. +At least one address is required; any number of addresses can be provided. +Each address should be a valid bech32 encoded string.`, + Example: fmt.Sprintf(` +$ %[1]s unsanction %[3]s +$ %[1]s unsanction %[2]s %[3]s +`, + exampleTxCmdBase, exampleTxAddr1, exampleTxAddr2), + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + flagSet := cmd.Flags() + + msgUnsanction := &sanction.MsgUnsanction{ + Addresses: args, + Authority: getAuthority(flagSet), + } + if err = msgUnsanction.ValidateBasic(); err != nil { + return err + } + + return govcli.GenerateOrBroadcastTxCLIAsGovProp(clientCtx, flagSet, msgUnsanction) + }, + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + addAuthorityFlagToCmd(cmd) + + return cmd +} + +// TxUpdateParamsCmd returns the command for submitting a MsgUpdateParams governance proposal tx. +func TxUpdateParamsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-params ", + Short: "Submit a governance proposal to update the sanction module's params", + Long: `Submit a governance proposal to update the sanction module's params. +Both and are required. +They must be coins or empty strings.`, + Example: fmt.Sprintf(` +$ %[1]s update-params 100%[2]s 150%[2]s +$ %[1]s update-params '' 50%[2]s +$ %[1]s update-params 75%[2]s '' +$ %[1]s update-params '' '' +`, + exampleTxCmdBase, sdk.DefaultBondDenom), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + flagSet := cmd.Flags() + + msgUpdateParams := &sanction.MsgUpdateParams{ + Params: &sanction.Params{}, + Authority: getAuthority(flagSet), + } + + if len(args[0]) > 0 { + msgUpdateParams.Params.ImmediateSanctionMinDeposit, err = sdk.ParseCoinsNormalized(args[0]) + if err != nil { + return fmt.Errorf("invalid immediate_sanction_min_deposit string %q: %w", args[0], err) + } + } + + if len(args[1]) > 0 { + msgUpdateParams.Params.ImmediateUnsanctionMinDeposit, err = sdk.ParseCoinsNormalized(args[1]) + if err != nil { + return fmt.Errorf("invalid immediate_unsanction_min_deposit string %q: %w", args[1], err) + } + } + + if err = msgUpdateParams.ValidateBasic(); err != nil { + return err + } + + return govcli.GenerateOrBroadcastTxCLIAsGovProp(clientCtx, flagSet, msgUpdateParams) + }, + } + + flags.AddTxFlagsToCmd(cmd) + govcli.AddGovPropFlagsToCmd(cmd) + addAuthorityFlagToCmd(cmd) + + return cmd +} + +// addAuthorityFlagToCmd adds the authority flag to a command. +func addAuthorityFlagToCmd(cmd *cobra.Command) { + // Note: Not setting a default here because the HRP might not yet be set correctly. + cmd.Flags().String(flags.FlagAuthority, "", "The authority to use. If not provided, a default is used") +} + +// getAuthority gets the authority string from the flagSet or returns the default. +func getAuthority(flagSet *pflag.FlagSet) string { + // Ignoring the error here since we really don't care, + // and it's easier if this just returns a string. + authority, _ := flagSet.GetString(flags.FlagAuthority) + if len(authority) > 0 { + return authority + } + return DefaultAuthorityAddr.String() +} diff --git a/x/sanction/module/module.go b/x/sanction/module/module.go index 92df492fd5e7..2d3ef01558ef 100644 --- a/x/sanction/module/module.go +++ b/x/sanction/module/module.go @@ -77,7 +77,7 @@ func (a AppModuleBasic) GetQueryCmd() *cobra.Command { // GetTxCmd returns the transaction commands for the sanction module func (a AppModuleBasic) GetTxCmd() *cobra.Command { - return nil + return cli.TxCmd() } // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the sanction module. From 2b3aa1ece9666a7551fb42769bfaa8d146454798 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 17:39:40 -0700 Subject: [PATCH 4/8] Add unit tests for GenerateOrBroadcastTxCLIAsGovProp. --- x/gov/client/cli/util_test.go | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/x/gov/client/cli/util_test.go b/x/gov/client/cli/util_test.go index 21aa72d769cb..4e592025bb20 100644 --- a/x/gov/client/cli/util_test.go +++ b/x/gov/client/cli/util_test.go @@ -2,10 +2,12 @@ package cli import ( "bytes" + "context" "encoding/base64" "fmt" "io" "os" + "runtime/debug" "strings" "testing" @@ -14,6 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/testutil" @@ -25,6 +28,23 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) +// convertPanicToErrorWithStack runs the provided runner. +// If it neither panics nor returns an error, nil is returned. +// If it returns an error, that error is returned. +// If it panics, an error with the panic message and stack trace is returned. +func convertPanicToErrorWithStack(runner func() error) (err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = fmt.Errorf("%w\n%s", e, string(debug.Stack())) + } else { + err = fmt.Errorf("%#v%s", r, string(debug.Stack())) + } + } + }() + return runner() +} + func TestParseSubmitLegacyProposalFlags(t *testing.T) { okJSON := testutil.WriteToNewTempFile(t, ` { @@ -461,3 +481,136 @@ func TestReadGovPropFlags(t *testing.T) { }) } } + +func TestGenerateOrBroadcastTxCLIAsGovProp(t *testing.T) { + fromAddr := sdk.AccAddress("another_from_address") + argDeposit := "--" + FlagDeposit + + tests := []struct { + name string + args []string + msgs []sdk.Msg + expErr []string + }{ + { + name: "control", + args: []string{argDeposit, "30goodcoin"}, + msgs: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: fromAddr.String(), + ValidatorAddress: sdk.ValAddress("1_validator_address_").String(), + Amount: sdk.NewInt64Coin("blargh", 42), + }, + &stakingtypes.MsgDelegate{ + DelegatorAddress: fromAddr.String(), + ValidatorAddress: sdk.ValAddress("2_validator_address_").String(), + Amount: sdk.NewInt64Coin("hgralb", 24), + }, + }, + // I don't care to test what happens in GenerateOrBroadcastTxCLI, + // which is the last thing called in GenerateOrBroadcastTxCLIAsGovProp. + // And setting it up so that GenerateOrBroadcastTxCLI has everything needed + // to not give an error is a major pain. + // But, I can test that execution got to that point by checking for + // a standard thing in the panic/error/stack. + expErr: []string{ + ".GenerateOrBroadcastTxCLI(", + ".GenerateOrBroadcastTxWithFactory(", + ".Factory.Prepare(", + "runtime error: invalid memory address or nil pointer dereference", + }, + }, + { + name: "no messages", + args: []string{argDeposit, "30emptycoin"}, + msgs: nil, + expErr: []string{"no messages to submit"}, + }, + { + name: "read gov prop flags fails", + args: []string{argDeposit, "notcoins"}, + msgs: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: fromAddr.String(), + ValidatorAddress: sdk.ValAddress("3_validator_address_").String(), + Amount: sdk.NewInt64Coin("gogogo", 99), + }, + }, + expErr: []string{"invalid deposit", "invalid decimal coin expression", "notcoins"}, + }, + { + name: "one message nil", + args: []string{argDeposit, "30nilcoin"}, + msgs: []sdk.Msg{nil}, + expErr: []string{"could not wrap message as Any", "Expecting non nil value to create a new Any"}, + }, + { + name: "two messages first nil", + args: []string{argDeposit, "32onecoin"}, + msgs: []sdk.Msg{ + nil, + &stakingtypes.MsgDelegate{ + DelegatorAddress: fromAddr.String(), + ValidatorAddress: sdk.ValAddress("4_validator_address_").String(), + Amount: sdk.NewInt64Coin("foundcoin", 200), + }, + }, + expErr: []string{"could not wrap message 0 () as Any", "Expecting non nil value to create a new Any"}, + }, + { + name: "two messages second nil", + args: []string{argDeposit, "31twocoin"}, + msgs: []sdk.Msg{ + &stakingtypes.MsgDelegate{ + DelegatorAddress: fromAddr.String(), + ValidatorAddress: sdk.ValAddress("5_validator_address_").String(), + Amount: sdk.NewInt64Coin("inccoin", 123), + }, + nil, + }, + expErr: []string{"could not wrap message 1 () as Any", "Expecting non nil value to create a new Any"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Create a dummy command to get stuff from. + cmd := &cobra.Command{ + Short: tc.name, + Run: func(cmd *cobra.Command, args []string) { + t.Errorf("The cmd for %q has run with the args %q, but Run shouldn't have been called.", tc.name, args) + }, + } + AddGovPropFlagsToCmd(cmd) + flags.AddTxFlagsToCmd(cmd) + + // Use it to parse the provided flags and get the resulting flagSet. + err := cmd.ParseFlags(tc.args) + require.NoError(t, err, "parsing test case args using cmd: %q", tc.args) + flagSet := cmd.Flags() + + // Give it a context and then retrieve it. + cmd.SetContext(context.WithValue(context.Background(), client.ClientContextKey, &client.Context{})) + clientCtx, err := client.GetClientTxContext(cmd) + require.NoError(t, err, "GetClientTxContext") + // Set the From Address so that the resulting proposal will have a proposer. + clientCtx.FromAddress = fromAddr + + // Run the function being tested. + testFunc := func() error { + return GenerateOrBroadcastTxCLIAsGovProp(clientCtx, flagSet, tc.msgs...) + } + err = convertPanicToErrorWithStack(testFunc) + + // Make sure the error has what's expected. + if len(tc.expErr) > 0 { + require.Error(t, err, "GenerateOrBroadcastTxCLIAsGovProp error") + for _, exp := range tc.expErr { + assert.ErrorContains(t, err, exp, "GenerateOrBroadcastTxCLIAsGovProp error") + } + } else { + require.NoError(t, err, "GenerateOrBroadcastTxCLIAsGovProp error") + } + }) + } +} From ad3db271bdfc578f3a37b78825c86a522d85c7fa Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 17:41:12 -0700 Subject: [PATCH 5/8] Rename TestSanctionValidatorImmediateUsingGovCmds (from TestSanctionValidatorImmediate) since we now have a tx command to do that too. --- x/sanction/client/testutil/cli_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/sanction/client/testutil/cli_test.go b/x/sanction/client/testutil/cli_test.go index 83ccd688787d..18f768a1d7ce 100644 --- a/x/sanction/client/testutil/cli_test.go +++ b/x/sanction/client/testutil/cli_test.go @@ -84,7 +84,7 @@ func TestIntegrationTestSuite(t *testing.T) { suite.Run(t, NewIntegrationTestSuite(cfg, &sanctionGen)) } -func (s *IntegrationTestSuite) TestSanctionValidatorImmediate() { +func (s *IntegrationTestSuite) TestSanctionValidatorImmediateUsingGovCmds() { // Wait 2 blocks to start this. That way, hopefully the query tests are done. // In between the two, create all the stuff to send. s.Require().NoError(s.network.WaitForNextBlock(), "wait for next block 1") From 6ae1ba2e86be4ef89e918d0f096219ae1b129b65 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Fri, 10 Feb 2023 18:05:54 -0700 Subject: [PATCH 6/8] Stub out some unit test stuff for the new Tx CLI stuff. --- x/sanction/client/testutil/tx_test.go | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 x/sanction/client/testutil/tx_test.go diff --git a/x/sanction/client/testutil/tx_test.go b/x/sanction/client/testutil/tx_test.go new file mode 100644 index 000000000000..9310cddeea65 --- /dev/null +++ b/x/sanction/client/testutil/tx_test.go @@ -0,0 +1,52 @@ +package testutil + +func (s *IntegrationTestSuite) TestTxSanctionCmd() { + // Cases: + // no addresses given + // one address good + // one address bad + // two addresses first bad + // two addresses second bad + // two addresses good + // bad authority given + // bad deposit + // + // Have each testcase have an expected proposal message. + // If it's not nil, get the last proposal and check its contents. + s.Fail("test not yet written") +} + +func (s *IntegrationTestSuite) TestTxUnsanctionCmd() { + // Cases: + // no addresses given + // one address good + // one address bad + // two addresses first bad + // two addresses second bad + // two addresses good + // bad authority given + // bad deposit + // + // Have each testcase have an expected proposal message. + // If it's not nil, get the last proposal and check its contents. + s.Fail("test not yet written") +} + +func (s *IntegrationTestSuite) TestTxUpdateParamsCmd() { + // Cases: + // no args given + // 1 arg given + // 3 args given + // coins coins + // empty coins + // coins empty + // empty empty + // bad coins + // coins bad + // bad authority given + // bad deposit + // + // Have each testcase have an expected proposal message. + // If it's not nil, get the last proposal and check its contents. + s.Fail("test not yet written") +} From 387fbf27abb814c8268ed71f4209b43d8a3a14e1 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Mon, 13 Feb 2023 14:15:54 -0700 Subject: [PATCH 7/8] Write the unit tests for the Sanction module's Tx commands. --- x/sanction/client/testutil/cli_test.go | 35 -- x/sanction/client/testutil/common_test.go | 71 +++- x/sanction/client/testutil/tx_test.go | 398 +++++++++++++++++++--- 3 files changed, 426 insertions(+), 78 deletions(-) diff --git a/x/sanction/client/testutil/cli_test.go b/x/sanction/client/testutil/cli_test.go index 18f768a1d7ce..7a67fdef918e 100644 --- a/x/sanction/client/testutil/cli_test.go +++ b/x/sanction/client/testutil/cli_test.go @@ -21,7 +21,6 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/cosmos/cosmos-sdk/testutil/network" sdk "github.com/cosmos/cosmos-sdk/types" - authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" gov "github.com/cosmos/cosmos-sdk/x/gov/types" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" @@ -285,37 +284,3 @@ func (s *IntegrationTestSuite) logHeight() int64 { s.T().Logf("Current height: %d", height) return height } - -func (s *IntegrationTestSuite) getAuthority() string { - args := []string{"gov", "--" + tmcli.OutputFlag, "json"} - outBW, err := cli.ExecTestCLICmd(s.clientCtx, authcli.QueryModuleAccountByNameCmd(), args) - s.Require().NoError(err, "ExecTestCLICmd q auth module-account gov") - outBz := outBW.Bytes() - s.T().Logf("q auth module-account gov output:\n%s", string(outBz)) - // example output: - // { - // "account": { - // "@type": "/cosmos.auth.v1beta1.ModuleAccount", - // "base_account": { - // "address": "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", - // "pub_key": null, - // "account_number": "9", - // "sequence": "0" - // }, - // "name": "gov", - // "permissions": ["burner"] - // } - // } - var output map[string]json.RawMessage - err = json.Unmarshal(outBz, &output) - s.Require().NoError(err, "Unmarshal output json") - var account map[string]json.RawMessage - err = json.Unmarshal(output["account"], &account) - s.Require().NoError(err, "Unmarshal account") - var baseAccount map[string]string - err = json.Unmarshal(account["base_account"], &baseAccount) - s.Require().NoError(err, "Unmarshal base_account") - rv := string(baseAccount["address"]) - s.T().Logf("authority: %q", rv) - return rv -} diff --git a/x/sanction/client/testutil/common_test.go b/x/sanction/client/testutil/common_test.go index 125488db3116..564244004eb5 100644 --- a/x/sanction/client/testutil/common_test.go +++ b/x/sanction/client/testutil/common_test.go @@ -1,11 +1,18 @@ package testutil import ( + "encoding/json" + "fmt" + "github.com/stretchr/testify/suite" + tmcli "github.com/tendermint/tendermint/libs/cli" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/cosmos/cosmos-sdk/testutil/network" sdk "github.com/cosmos/cosmos-sdk/types" + authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/cosmos/cosmos-sdk/x/sanction" "github.com/cosmos/cosmos-sdk/x/sanction/testutil" ) @@ -17,7 +24,9 @@ type IntegrationTestSuite struct { network *network.Network clientCtx client.Context - valAddr sdk.AccAddress + commonArgs []string + valAddr sdk.AccAddress + authority string sanctionGenesis *sanction.GenesisState } @@ -41,6 +50,14 @@ func (s *IntegrationTestSuite) SetupSuite() { s.clientCtx = s.network.Validators[0].ClientCtx s.valAddr = s.network.Validators[0].Address + + s.commonArgs = []string{ + fmt.Sprintf("--%s", flags.FlagFrom), s.valAddr.String(), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, s.bondCoins(10).String()), + } + } func (s *IntegrationTestSuite) TearDownSuite() { @@ -52,3 +69,55 @@ func (s *IntegrationTestSuite) TearDownSuite() { func (s *IntegrationTestSuite) assertErrorContents(theError error, contains []string, msgAndArgs ...interface{}) bool { return testutil.AssertErrorContents(s.T(), theError, contains, msgAndArgs...) } + +// bondCoin creates an sdk.Coin with the bond-denom in the amount provided. +func (s *IntegrationTestSuite) bondCoin(amt int64) sdk.Coin { + return sdk.NewInt64Coin(s.cfg.BondDenom, amt) +} + +// bondCoins creates an sdk.Coins with the bond-denom in the amount provided. +func (s *IntegrationTestSuite) bondCoins(amt int64) sdk.Coins { + return sdk.NewCoins(s.bondCoin(amt)) +} + +// appendCommonFlagsTo adds this suite's common flags to the end of the provided arguments. +func (s *IntegrationTestSuite) appendCommonArgsTo(args ...string) []string { + return append(args, s.commonArgs...) +} + +func (s *IntegrationTestSuite) getAuthority() string { + if len(s.authority) > 0 { + return s.authority + } + args := []string{"gov", "--" + tmcli.OutputFlag, "json"} + outBW, err := cli.ExecTestCLICmd(s.clientCtx, authcli.QueryModuleAccountByNameCmd(), args) + s.Require().NoError(err, "ExecTestCLICmd q auth module-account gov") + outBz := outBW.Bytes() + s.T().Logf("q auth module-account gov output:\n%s", string(outBz)) + // example output: + // { + // "account": { + // "@type": "/cosmos.auth.v1beta1.ModuleAccount", + // "base_account": { + // "address": "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + // "pub_key": null, + // "account_number": "9", + // "sequence": "0" + // }, + // "name": "gov", + // "permissions": ["burner"] + // } + // } + var output map[string]json.RawMessage + err = json.Unmarshal(outBz, &output) + s.Require().NoError(err, "Unmarshal output json") + var account map[string]json.RawMessage + err = json.Unmarshal(output["account"], &account) + s.Require().NoError(err, "Unmarshal account") + var baseAccount map[string]string + err = json.Unmarshal(account["base_account"], &baseAccount) + s.Require().NoError(err, "Unmarshal base_account") + s.authority = baseAccount["address"] + s.T().Logf("authority: %q", s.authority) + return s.authority +} diff --git a/x/sanction/client/testutil/tx_test.go b/x/sanction/client/testutil/tx_test.go index 9310cddeea65..036bcebc9d4f 100644 --- a/x/sanction/client/testutil/tx_test.go +++ b/x/sanction/client/testutil/tx_test.go @@ -1,52 +1,366 @@ package testutil +import ( + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/sanction" + client "github.com/cosmos/cosmos-sdk/x/sanction/client/cli" +) + +// assertGovPropMsg gets a gov prop and makes sure it has one specific message. +func (s *IntegrationTestSuite) 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 on %T", msg) { + return false + } + + getPropCmd := govcli.GetCmdQueryProposal() + propOutBW, err := cli.ExecTestCLICmd(s.clientCtx, getPropCmd, []string{propID, "--output", "json"}) + propOut := propOutBW.String() + s.T().Logf("Query proposal %s output:\n%s", propID, propOut) + if !s.Assert().NoError(err, "GetCmdQueryProposal error") { + return false + } + + var prop govv1.Proposal + err = s.clientCtx.Codec.UnmarshalJSON([]byte(propOut), &prop) + if !s.Assert().NoError(err, "UnmarshalJSON on proposal response") { + return false + } + if !s.Assert().Len(prop.Messages, 1, "number of messages in proposal") { + return false + } + if !s.Assert().Equal(expPropMsgAny, prop.Messages[0], "the message in the proposal") { + return false + } + + return true +} + +// findProposalID looks through the provided response to find a governance proposal id. +// If one is found, it's returned (as a string). Otherwise, an empty string is returned. +func (s *IntegrationTestSuite) findProposalID(resp *sdk.TxResponse) string { + for _, event := range resp.Events { + if event.Type == "submit_proposal" { + for _, attr := range event.Attributes { + if string(attr.Key) == "proposal_id" { + return string(attr.Value) + } + } + } + } + return "" +} + func (s *IntegrationTestSuite) TestTxSanctionCmd() { - // Cases: - // no addresses given - // one address good - // one address bad - // two addresses first bad - // two addresses second bad - // two addresses good - // bad authority given - // bad deposit - // - // Have each testcase have an expected proposal message. - // If it's not nil, get the last proposal and check its contents. - s.Fail("test not yet written") + authority := s.getAuthority() + addr1 := sdk.AccAddress("1_address_test_test_").String() + addr2 := sdk.AccAddress("2_address_test_test_").String() + + tests := []struct { + name string + args []string + expErr []string + expPropMsg *sanction.MsgSanction + }{ + { + name: "no addresses given", + args: []string{}, + expErr: []string{"requires at least 1 arg(s), only received 0"}, + }, + { + name: "one address good", + args: []string{addr1}, + expPropMsg: &sanction.MsgSanction{ + Addresses: []string{addr1}, + Authority: authority, + }, + }, + { + name: "one address bad", + args: []string{"thisis1addrthatisbad"}, + expErr: []string{"addresses[0]", `"thisis1addrthatisbad"`, "decoding bech32 failed"}, + }, + { + name: "two addresses first bad", + args: []string{"another1badaddr", addr2}, + expErr: []string{"addresses[0]", `"another1badaddr"`, "decoding bech32 failed"}, + }, + { + name: "two addresses second bad", + args: []string{addr1, "athird1badaddress"}, + expErr: []string{"addresses[1]", `"athird1badaddress"`, "decoding bech32 failed"}, + }, + { + name: "two addresses good", + args: []string{addr1, addr2}, + expPropMsg: &sanction.MsgSanction{ + Addresses: []string{addr1, addr2}, + Authority: authority, + }, + }, + { + name: "bad authority", + args: []string{addr1, "--" + flags.FlagAuthority, "bad1auth34sd2"}, + expErr: []string{"authority", `"bad1auth34sd2"`, "decoding bech32 failed"}, + }, + { + name: "bad deposit", + args: []string{addr1, "--" + govcli.FlagDeposit, "notcoins"}, + expErr: []string{"invalid deposit", "notcoins"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + cmd := client.TxSanctionCmd() + cmdFuncName := "TxSanctionCmd" + args := s.appendCommonArgsTo(tc.args...) + + outBW, err := cli.ExecTestCLICmd(s.clientCtx, cmd, args) + out := outBW.String() + s.T().Logf("Output:\n%s", out) + s.assertErrorContents(err, tc.expErr, "%s error", cmdFuncName) + for _, expErr := range tc.expErr { + s.Assert().Contains(out, expErr, "%s output with error", cmdFuncName) + } + + var propID string + if len(tc.expErr) == 0 { + var txResp sdk.TxResponse + err = s.clientCtx.Codec.UnmarshalJSON([]byte(out), &txResp) + if s.Assert().NoError(err, "UnmarshalJSON on %s", cmdFuncName) { + s.Assert().Equal(0, int(txResp.Code), "%s response code", cmdFuncName) + } + propID = s.findProposalID(&txResp) + } + + if tc.expPropMsg != nil { + s.assertGovPropMsg(propID, tc.expPropMsg) + } + }) + } } func (s *IntegrationTestSuite) TestTxUnsanctionCmd() { - // Cases: - // no addresses given - // one address good - // one address bad - // two addresses first bad - // two addresses second bad - // two addresses good - // bad authority given - // bad deposit - // - // Have each testcase have an expected proposal message. - // If it's not nil, get the last proposal and check its contents. - s.Fail("test not yet written") + authority := s.getAuthority() + addr1 := sdk.AccAddress("1_address_untest____").String() + addr2 := sdk.AccAddress("2_address_untest____").String() + + tests := []struct { + name string + args []string + expErr []string + expPropMsg *sanction.MsgUnsanction + }{ + { + name: "no addresses given", + args: []string{}, + expErr: []string{"requires at least 1 arg(s), only received 0"}, + }, + { + name: "one address good", + args: []string{addr1}, + expPropMsg: &sanction.MsgUnsanction{ + Addresses: []string{addr1}, + Authority: authority, + }, + }, + { + name: "one address bad", + args: []string{"thisis1addrthatisbad"}, + expErr: []string{"addresses[0]", `"thisis1addrthatisbad"`, "decoding bech32 failed"}, + }, + { + name: "two addresses first bad", + args: []string{"another1badaddr", addr2}, + expErr: []string{"addresses[0]", `"another1badaddr"`, "decoding bech32 failed"}, + }, + { + name: "two addresses second bad", + args: []string{addr1, "athird1badaddress"}, + expErr: []string{"addresses[1]", `"athird1badaddress"`, "decoding bech32 failed"}, + }, + { + name: "two addresses good", + args: []string{addr1, addr2}, + expPropMsg: &sanction.MsgUnsanction{ + Addresses: []string{addr1, addr2}, + Authority: authority, + }, + }, + { + name: "bad authority", + args: []string{addr1, "--" + flags.FlagAuthority, "bad1auth34sd2"}, + expErr: []string{"authority", `"bad1auth34sd2"`, "decoding bech32 failed"}, + }, + { + name: "bad deposit", + args: []string{addr1, "--" + govcli.FlagDeposit, "notcoins"}, + expErr: []string{"invalid deposit", "notcoins"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + cmd := client.TxUnsanctionCmd() + cmdFuncName := "TxUnsanctionCmd" + args := s.appendCommonArgsTo(tc.args...) + + outBW, err := cli.ExecTestCLICmd(s.clientCtx, cmd, args) + out := outBW.String() + s.T().Logf("Output:\n%s", out) + s.assertErrorContents(err, tc.expErr, "%s error", cmdFuncName) + for _, expErr := range tc.expErr { + s.Assert().Contains(out, expErr, "%s output with error", cmdFuncName) + } + + var propID string + if len(tc.expErr) == 0 { + var txResp sdk.TxResponse + err = s.clientCtx.Codec.UnmarshalJSON([]byte(out), &txResp) + if s.Assert().NoError(err, "UnmarshalJSON on %s", cmdFuncName) { + s.Assert().Equal(0, int(txResp.Code), "%s response code", cmdFuncName) + } + propID = s.findProposalID(&txResp) + } + + if tc.expPropMsg != nil { + s.assertGovPropMsg(propID, tc.expPropMsg) + } + }) + } } func (s *IntegrationTestSuite) TestTxUpdateParamsCmd() { - // Cases: - // no args given - // 1 arg given - // 3 args given - // coins coins - // empty coins - // coins empty - // empty empty - // bad coins - // coins bad - // bad authority given - // bad deposit - // - // Have each testcase have an expected proposal message. - // If it's not nil, get the last proposal and check its contents. - s.Fail("test not yet written") + authority := s.getAuthority() + + tests := []struct { + name string + args []string + expErr []string + expPropMsg *sanction.MsgUpdateParams + }{ + { + name: "no args", + args: []string{}, + expErr: []string{"accepts 2 arg(s), received 0"}, + }, + { + name: "one arg", + args: []string{"arg1"}, + expErr: []string{"accepts 2 arg(s), received 1"}, + }, + { + name: "three args", + args: []string{"arg1", "arg2", "arg3"}, + expErr: []string{"accepts 2 arg(s), received 3"}, + }, + { + name: "coins coins", + args: []string{"1acoin", "2bcoin"}, + expPropMsg: &sanction.MsgUpdateParams{ + Params: &sanction.Params{ + ImmediateSanctionMinDeposit: sdk.NewCoins(sdk.NewInt64Coin("acoin", 1)), + ImmediateUnsanctionMinDeposit: sdk.NewCoins(sdk.NewInt64Coin("bcoin", 2)), + }, + Authority: authority, + }, + }, + { + name: "empty coins", + args: []string{"", "3ccoin"}, + expPropMsg: &sanction.MsgUpdateParams{ + Params: &sanction.Params{ + ImmediateSanctionMinDeposit: nil, + ImmediateUnsanctionMinDeposit: sdk.NewCoins(sdk.NewInt64Coin("ccoin", 3)), + }, + Authority: authority, + }, + }, + { + name: "coins empty", + args: []string{"4dcoin", ""}, + expPropMsg: &sanction.MsgUpdateParams{ + Params: &sanction.Params{ + ImmediateSanctionMinDeposit: sdk.NewCoins(sdk.NewInt64Coin("dcoin", 4)), + ImmediateUnsanctionMinDeposit: nil, + }, + Authority: authority, + }, + }, + { + name: "empty empty", + args: []string{"", ""}, + expPropMsg: &sanction.MsgUpdateParams{ + Params: &sanction.Params{ + ImmediateSanctionMinDeposit: nil, + ImmediateUnsanctionMinDeposit: nil, + }, + Authority: authority, + }, + }, + { + name: "bad good", + args: []string{"firscoinsbad", "5ecoin"}, + expErr: []string{"invalid immediate_sanction_min_deposit", `"firscoinsbad"`}, + }, + { + name: "good bad", + args: []string{"6fcoin", "secondcoinsbad"}, + expErr: []string{"invalid immediate_unsanction_min_deposit", `"secondcoinsbad"`}, + }, + { + name: "bad authority", + args: []string{"", "", "--" + flags.FlagAuthority, "bad1auth34sd2"}, + expErr: []string{"authority", `"bad1auth34sd2"`, "decoding bech32 failed"}, + }, + { + name: "bad deposit", + args: []string{"", "", "--" + govcli.FlagDeposit, "notcoins"}, + expErr: []string{"invalid deposit", "notcoins"}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + cmd := client.TxUpdateParamsCmd() + cmdFuncName := "TxUpdateParamsCmd" + args := s.appendCommonArgsTo(tc.args...) + + outBW, err := cli.ExecTestCLICmd(s.clientCtx, cmd, args) + out := outBW.String() + s.T().Logf("Output:\n%s", out) + s.assertErrorContents(err, tc.expErr, "%s error", cmdFuncName) + for _, expErr := range tc.expErr { + s.Assert().Contains(out, expErr, "%s output with error", cmdFuncName) + } + + var propID string + if len(tc.expErr) == 0 { + var txResp sdk.TxResponse + err = s.clientCtx.Codec.UnmarshalJSON([]byte(out), &txResp) + if s.Assert().NoError(err, "UnmarshalJSON on %s", cmdFuncName) { + s.Assert().Equal(0, int(txResp.Code), "%s response code", cmdFuncName) + } + propID = s.findProposalID(&txResp) + } + + if tc.expPropMsg != nil { + s.assertGovPropMsg(propID, tc.expPropMsg) + } + }) + } } From aa00ad4597642623ccebb99fb1e8b86e1a08d9cf Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Mon, 13 Feb 2023 14:19:51 -0700 Subject: [PATCH 8/8] Add changelog entry. --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bac07703bc..5aaf63c2fc6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased -* nothing +### Features + +* [#510](https://github.com/provenance-io/cosmos-sdk/pull/510) Add Sanction Tx commands. ---