diff --git a/app/experimental_appconfig.go b/app/experimental_appconfig.go index 820b49e1b8..0a44bffdab 100644 --- a/app/experimental_appconfig.go +++ b/app/experimental_appconfig.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/types/module" - authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" moduletypes "github.com/regen-network/regen-ledger/types/module" servermodule "github.com/regen-network/regen-ledger/types/module/server" @@ -29,7 +28,7 @@ func setCustomModules(app *RegenApp, interfaceRegistry types.InterfaceRegistry) newModuleManager := servermodule.NewManager(app.BaseApp, codec.NewProtoCodec(interfaceRegistry)) // BEGIN HACK: this is a total, ugly hack until x/auth supports ADR 033 or we have a suitable alternative - groupModule := group.Module{AccountKeeper: authkeeper.AccountKeeper{}} + groupModule := group.Module{AccountKeeper: app.AccountKeeper} // use a separate newModules from the global NewModules here because we need to pass state into the group module newModules := []moduletypes.Module{ ecocredit.Module{}, diff --git a/docs/modules/data/protobuf.md b/docs/modules/data/protobuf.md index 91dd136005..11da3122be 100644 --- a/docs/modules/data/protobuf.md +++ b/docs/modules/data/protobuf.md @@ -571,4 +571,3 @@ The sender in StoreRawData is not attesting to the veracity of the underlying da | bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | | bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | - diff --git a/types/module/configurator.go b/types/module/configurator.go new file mode 100644 index 0000000000..efe59bd6bf --- /dev/null +++ b/types/module/configurator.go @@ -0,0 +1,43 @@ +package module + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdkmodule "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gogo/protobuf/grpc" +) + +// Configurator extends the cosmos sdk Configurator interface +// with Marshaler() +type Configurator interface { + sdkmodule.Configurator + + Marshaler() codec.Marshaler +} + +type configurator struct { + msgServer grpc.Server + queryServer grpc.Server + cdc codec.Marshaler +} + +// NewConfigurator returns a new Configurator instance +func NewConfigurator(msgServer grpc.Server, queryServer grpc.Server, cdc codec.Marshaler) Configurator { + return configurator{msgServer: msgServer, queryServer: queryServer, cdc: cdc} +} + +var _ Configurator = configurator{} + +// MsgServer implements the Configurator.MsgServer method +func (c configurator) MsgServer() grpc.Server { + return c.msgServer +} + +// QueryServer implements the Configurator.QueryServer method +func (c configurator) QueryServer() grpc.Server { + return c.queryServer +} + +// Marshaler implements the Configurator.Marshaler method +func (c configurator) Marshaler() codec.Marshaler { + return c.cdc +} diff --git a/types/module/server/manager.go b/types/module/server/manager.go index da2afbdcfc..926510ee5a 100644 --- a/types/module/server/manager.go +++ b/types/module/server/manager.go @@ -98,6 +98,15 @@ func (mm *Manager) RegisterModules(modules []module.Module) error { serverMod.RegisterServices(cfg) + // If mod implements LegacyRouteModule, register module route. + // This is currently used for the group module as part of #218. + routeMod, ok := mod.(LegacyRouteModule) + if ok { + if r := routeMod.Route(cfg); !r.Empty() { + mm.baseApp.Router().AddRoute(r) + } + } + for typ := range cfg.requiredServices { mm.requiredServices[typ] = true } diff --git a/types/module/server/module.go b/types/module/server/module.go index 842ea1225d..440188893d 100644 --- a/types/module/server/module.go +++ b/types/module/server/module.go @@ -25,3 +25,10 @@ type Configurator interface { // TODO: remove once #225 addressed Router() sdk.Router } + +// LegacyRouteModule is the module type that a module must implement +// to support legacy sdk.Msg routing. +// This is currently used for the group module as part of #218. +type LegacyRouteModule interface { + Route(Configurator) sdk.Route +} diff --git a/x/group/client/query.go b/x/group/client/query.go new file mode 100644 index 0000000000..4773681668 --- /dev/null +++ b/x/group/client/query.go @@ -0,0 +1,440 @@ +package client + +import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/regen-network/regen-ledger/x/group" + "github.com/spf13/cobra" +) + +// QueryCmd returns the cli query commands for the group module. +func QueryCmd(name string) *cobra.Command { + queryCmd := &cobra.Command{ + Use: name, + Short: "Querying commands for the group module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + queryCmd.AddCommand( + QueryGroupInfoCmd(), + QueryGroupAccountInfoCmd(), + QueryGroupMembersCmd(), + QueryGroupsByAdminCmd(), + QueryGroupAccountsByGroupCmd(), + QueryGroupAccountsByAdminCmd(), + QueryProposalCmd(), + QueryProposalsByGroupAccountCmd(), + QueryVoteByProposalVoterCmd(), + QueryVotesByProposalCmd(), + QueryVotesByVoterCmd(), + ) + + return queryCmd +} + +// QueryGroupInfoCmd creates a CLI command for Query/GroupInfo. +func QueryGroupInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group-info [id]", + Short: "Query for group info by group id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupInfo(cmd.Context(), &group.QueryGroupInfoRequest{ + GroupId: group.ID(groupID), + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res.Info) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryGroupAccountInfoCmd creates a CLI command for Query/GroupAccountInfo. +func QueryGroupAccountInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group-account-info [group-account]", + Short: "Query for group account info by group account address", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupAccountInfo(cmd.Context(), &group.QueryGroupAccountInfoRequest{ + GroupAccount: args[0], + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res.Info) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryGroupMembersCmd creates a CLI command for Query/GroupMembers. +func QueryGroupMembersCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group-members [id]", + Short: "Query for group members by group id with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupMembers(cmd.Context(), &group.QueryGroupMembersRequest{ + GroupId: group.ID(groupID), + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryGroupsByAdminCmd creates a CLI command for Query/GroupsByAdmin. +func QueryGroupsByAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "groups-by-admin [admin]", + Short: "Query for groups by admin account address with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupsByAdmin(cmd.Context(), &group.QueryGroupsByAdminRequest{ + Admin: args[0], + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryGroupAccountsByGroupCmd creates a CLI command for Query/GroupAccountsByGroup. +func QueryGroupAccountsByGroupCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group-accounts-by-group [group-id]", + Short: "Query for group accounts by group id with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupAccountsByGroup(cmd.Context(), &group.QueryGroupAccountsByGroupRequest{ + GroupId: group.ID(groupID), + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryGroupAccountsByAdminCmd creates a CLI command for Query/GroupAccountsByAdmin. +func QueryGroupAccountsByAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "group-accounts-by-admin [admin]", + Short: "Query for group accounts by admin account address with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.GroupAccountsByAdmin(cmd.Context(), &group.QueryGroupAccountsByAdminRequest{ + Admin: args[0], + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryProposalCmd creates a CLI command for Query/Proposal. +func QueryProposalCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "proposal [id]", + Short: "Query for proposal by id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.Proposal(cmd.Context(), &group.QueryProposalRequest{ + ProposalId: group.ProposalID(proposalID), + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryProposalsByGroupAccountCmd creates a CLI command for Query/ProposalsByGroupAccount. +func QueryProposalsByGroupAccountCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "proposals-by-group-account [group-account]", + Short: "Query for proposals by group account address with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.ProposalsByGroupAccount(cmd.Context(), &group.QueryProposalsByGroupAccountRequest{ + GroupAccount: args[0], + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryVoteByProposalVoterCmd creates a CLI command for Query/VoteByProposalVoter. +func QueryVoteByProposalVoterCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "vote [proposal-id] [voter]", + Short: "Query for vote by proposal id and voter account address", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.VoteByProposalVoter(cmd.Context(), &group.QueryVoteByProposalVoterRequest{ + ProposalId: group.ProposalID(proposalID), + Voter: args[1], + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryVotesByProposalCmd creates a CLI command for Query/VotesByProposal. +func QueryVotesByProposalCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "votes-by-proposal [proposal-id]", + Short: "Query for votes by proposal id with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.VotesByProposal(cmd.Context(), &group.QueryVotesByProposalRequest{ + ProposalId: group.ProposalID(proposalID), + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// QueryVotesByVoterCmd creates a CLI command for Query/VotesByVoter. +func QueryVotesByVoterCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "votes-by-voter [voter]", + Short: "Query for votes by voter account address with pagination flags", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + queryClient := group.NewQueryClient(clientCtx) + + res, err := queryClient.VotesByVoter(cmd.Context(), &group.QueryVotesByVoterRequest{ + Voter: args[0], + Pagination: pageReq, + }) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/group/client/testsuite/cli_test.go b/x/group/client/testsuite/cli_test.go new file mode 100644 index 0000000000..8f12300777 --- /dev/null +++ b/x/group/client/testsuite/cli_test.go @@ -0,0 +1,2051 @@ +// +build experimental + +package testsuite + +import ( + "fmt" + "strconv" + "strings" + "testing" + + tmcli "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/testutil" + banktestutil "github.com/cosmos/cosmos-sdk/x/bank/client/testutil" + "github.com/gogo/protobuf/proto" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/regen-network/regen-ledger/testutil/cli" + "github.com/regen-network/regen-ledger/testutil/network" + + "github.com/regen-network/regen-ledger/x/group" + "github.com/regen-network/regen-ledger/x/group/client" + "github.com/stretchr/testify/suite" +) + +type IntegrationTestSuite struct { + suite.Suite + + cfg network.Config + network *network.Network + + group *group.GroupInfo + groupAccount *group.GroupAccountInfo + groupAccount2 *group.GroupAccountInfo + groupAccount3 *group.GroupAccountInfo + proposal *group.Proposal + vote *group.Vote +} + +const validMetadata = "AQ==" + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + cfg := network.DefaultConfig() + cfg.NumValidators = 2 + + s.cfg = cfg + s.network = network.New(s.T(), cfg) + + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) + + val := s.network.Validators[0] + + // create a new account + info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewValidator", keyring.English, sdk.FullFundraiserPath, hd.Secp256k1) + s.Require().NoError(err) + + account := sdk.AccAddress(info.GetPubKey().Address()) + _, err = banktestutil.MsgSendExec( + val.ClientCtx, + val.Address, + account, + sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(2000))), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + ) + s.Require().NoError(err) + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + // create a group + validMembers := fmt.Sprintf(`[{ + "address": "%s", + "weight": "1", + "metadata": "%s" + }]`, val.Address.String(), validMetadata) + validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) + out, err := cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateGroupCmd(), + append( + []string{ + val.Address.String(), + validMetadata, + validMembersFile.Name(), + }, + commonFlags..., + ), + ) + + s.Require().NoError(err, out.String()) + var txResp = sdk.TxResponse{} + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txResp), out.String()) + s.Require().Equal(uint32(0), txResp.Code, out.String()) + + s.group = &group.GroupInfo{GroupId: group.ID(1), Admin: val.Address.String(), Metadata: []byte{1}, TotalWeight: "1", Version: 1} + + // create 3 group accounts + for i := 0; i < 3; i++ { + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateGroupAccountCmd(), + append( + []string{ + val.Address.String(), + "1", + validMetadata, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"30000s\"}", + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txResp), out.String()) + s.Require().Equal(uint32(0), txResp.Code, out.String()) + + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.QueryGroupAccountsByGroupCmd(), []string{"1", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}) + s.Require().NoError(err, out.String()) + } + + var res group.QueryGroupAccountsByGroupResponse + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.GroupAccounts), 3) + s.groupAccount = res.GroupAccounts[0] + s.groupAccount2 = res.GroupAccounts[1] + s.groupAccount3 = res.GroupAccounts[2] + + // create a proposal + validTxFileName := getTxSendFileName(s, s.groupAccount.GroupAccount, val.Address.String()) + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateProposalCmd(), + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txResp), out.String()) + s.Require().Equal(uint32(0), txResp.Code, out.String()) + + // vote + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.MsgVoteCmd(), + append( + []string{ + "1", + val.Address.String(), + "CHOICE_YES", + "", + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txResp), out.String()) + s.Require().Equal(uint32(0), txResp.Code, out.String()) + + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.QueryProposalCmd(), []string{"1", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}) + s.Require().NoError(err, out.String()) + + var proposalRes group.QueryProposalResponse + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &proposalRes)) + s.proposal = proposalRes.Proposal + + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.QueryVoteByProposalVoterCmd(), []string{"1", val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}) + s.Require().NoError(err, out.String()) + + var voteRes group.QueryVoteByProposalVoterResponse + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &voteRes)) + s.vote = voteRes.Vote +} + +func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +func (s *IntegrationTestSuite) TestQueryGroupInfo() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + }{ + { + "group not found", + []string{"12345", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "not found: invalid request", + 0, + }, + { + "group id invalid", + []string{"", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + 0, + }, + { + "group found", + []string{strconv.FormatUint(s.group.GroupId.Uint64(), 10), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupInfoCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var g group.GroupInfo + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &g)) + s.Require().Equal(s.group.GroupId, g.GroupId) + s.Require().Equal(s.group.Admin, g.Admin) + s.Require().Equal(s.group.TotalWeight, g.TotalWeight) + s.Require().Equal(s.group.Metadata, g.Metadata) + s.Require().Equal(s.group.Version, g.Version) + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryGroupMembers() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectMembers []*group.GroupMember + }{ + { + "no group", + []string{"12345", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupMember{}, + }, + { + "members found", + []string{strconv.FormatUint(s.group.GroupId.Uint64(), 10), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupMember{ + { + GroupId: s.group.GroupId, + Member: &group.Member{ + Address: val.Address.String(), + Weight: "1", + Metadata: []byte{1}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupMembersCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryGroupMembersResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.Members), len(tc.expectMembers)) + for i := range res.Members { + s.Require().Equal(res.Members[i].GroupId, tc.expectMembers[i].GroupId) + s.Require().Equal(res.Members[i].Member.Address, tc.expectMembers[i].Member.Address) + s.Require().Equal(res.Members[i].Member.Metadata, tc.expectMembers[i].Member.Metadata) + s.Require().Equal(res.Members[i].Member.Weight, tc.expectMembers[i].Member.Weight) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryGroupsByAdmin() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectGroups []*group.GroupInfo + }{ + { + "invalid admin address", + []string{"invalid"}, + true, + "decoding bech32 failed: invalid bech32 string", + 0, + []*group.GroupInfo{}, + }, + { + "no group", + []string{s.network.Validators[1].Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupInfo{}, + }, + { + "found groups", + []string{val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupInfo{ + s.group, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupsByAdminCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryGroupsByAdminResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.Groups), len(tc.expectGroups)) + for i := range res.Groups { + s.Require().Equal(res.Groups[i].GroupId, tc.expectGroups[i].GroupId) + s.Require().Equal(res.Groups[i].Metadata, tc.expectGroups[i].Metadata) + s.Require().Equal(res.Groups[i].Version, tc.expectGroups[i].Version) + s.Require().Equal(res.Groups[i].TotalWeight, tc.expectGroups[i].TotalWeight) + s.Require().Equal(res.Groups[i].Admin, tc.expectGroups[i].Admin) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryGroupAccountInfo() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + }{ + { + "invalid account address", + []string{"invalid", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "decoding bech32 failed: invalid bech32", + 0, + }, + { + "group account not found", + []string{val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "not found: invalid request", + 0, + }, + { + "group account found", + []string{s.groupAccount.GroupAccount, fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupAccountInfoCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var g group.GroupAccountInfo + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &g)) + s.Require().Equal(s.groupAccount.GroupId, g.GroupId) + s.Require().Equal(s.groupAccount.GroupAccount, g.GroupAccount) + s.Require().Equal(s.groupAccount.Admin, g.Admin) + s.Require().Equal(s.groupAccount.Metadata, g.Metadata) + s.Require().Equal(s.groupAccount.Version, g.Version) + s.Require().Equal(s.groupAccount.GetDecisionPolicy(), g.GetDecisionPolicy()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryGroupAccountsByGroup() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectGroupAccounts []*group.GroupAccountInfo + }{ + { + "invalid group id", + []string{""}, + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + 0, + []*group.GroupAccountInfo{}, + }, + { + "no group account", + []string{"12345", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupAccountInfo{}, + }, + { + "found group accounts", + []string{strconv.FormatUint(s.group.GroupId.Uint64(), 10), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupAccountInfo{ + s.groupAccount, + s.groupAccount2, + s.groupAccount3, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupAccountsByGroupCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryGroupAccountsByGroupResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.GroupAccounts), len(tc.expectGroupAccounts)) + for i := range res.GroupAccounts { + s.Require().Equal(res.GroupAccounts[i].GroupId, tc.expectGroupAccounts[i].GroupId) + s.Require().Equal(res.GroupAccounts[i].Metadata, tc.expectGroupAccounts[i].Metadata) + s.Require().Equal(res.GroupAccounts[i].Version, tc.expectGroupAccounts[i].Version) + s.Require().Equal(res.GroupAccounts[i].Admin, tc.expectGroupAccounts[i].Admin) + s.Require().Equal(res.GroupAccounts[i].GetDecisionPolicy(), tc.expectGroupAccounts[i].GetDecisionPolicy()) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryGroupAccountsByAdmin() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectGroupAccounts []*group.GroupAccountInfo + }{ + { + "invalid admin address", + []string{"invalid"}, + true, + "decoding bech32 failed: invalid bech32 string", + 0, + []*group.GroupAccountInfo{}, + }, + { + "no group account", + []string{s.network.Validators[1].Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupAccountInfo{}, + }, + { + "found group accounts", + []string{val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.GroupAccountInfo{ + s.groupAccount, + s.groupAccount2, + s.groupAccount3, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryGroupAccountsByAdminCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryGroupAccountsByAdminResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.GroupAccounts), len(tc.expectGroupAccounts)) + for i := range res.GroupAccounts { + s.Require().Equal(res.GroupAccounts[i].GroupId, tc.expectGroupAccounts[i].GroupId) + s.Require().Equal(res.GroupAccounts[i].Metadata, tc.expectGroupAccounts[i].Metadata) + s.Require().Equal(res.GroupAccounts[i].Version, tc.expectGroupAccounts[i].Version) + s.Require().Equal(res.GroupAccounts[i].Admin, tc.expectGroupAccounts[i].Admin) + s.Require().Equal(res.GroupAccounts[i].GetDecisionPolicy(), tc.expectGroupAccounts[i].GetDecisionPolicy()) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryProposal() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + }{ + { + "not found", + []string{"12345", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "not found", + 0, + }, + { + "invalid proposal id", + []string{"", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryProposalCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryProposalsByGroupAccount() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectProposals []*group.Proposal + }{ + { + "invalid group account address", + []string{"invalid"}, + true, + "decoding bech32 failed: invalid bech32 string", + 0, + []*group.Proposal{}, + }, + { + "no group account", + []string{s.network.Validators[1].Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.Proposal{}, + }, + { + "found proposals", + []string{s.groupAccount.GroupAccount, fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.Proposal{ + s.proposal, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryProposalsByGroupAccountCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryProposalsByGroupAccountResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.Proposals), len(tc.expectProposals)) + for i := range res.Proposals { + s.Require().Equal(res.Proposals[i], tc.expectProposals[i]) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryVoteByProposalVoter() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + }{ + { + "invalid voter address", + []string{"1", "invalid", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "decoding bech32 failed: invalid bech32", + 0, + }, + { + "invalid proposal id", + []string{"", val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryVoteByProposalVoterCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryVotesByProposal() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectVotes []*group.Vote + }{ + { + "invalid proposal id", + []string{"", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + 0, + []*group.Vote{}, + }, + { + "no votes", + []string{"12345", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.Vote{}, + }, + { + "found votes", + []string{"1", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.Vote{ + s.vote, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryVotesByProposalCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryVotesByProposalResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.Votes), len(tc.expectVotes)) + for i := range res.Votes { + s.Require().Equal(res.Votes[i], tc.expectVotes[i]) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestQueryVotesByVoter() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + expectedCode uint32 + expectVotes []*group.Vote + }{ + { + "invalid voter address", + []string{"abcd", fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "decoding bech32 failed: invalid bech32", + 0, + []*group.Vote{}, + }, + { + "no votes", + []string{s.groupAccount.GroupAccount, fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + true, + "", + 0, + []*group.Vote{}, + }, + { + "found votes", + []string{val.Address.String(), fmt.Sprintf("--%s=json", tmcli.OutputFlag)}, + false, + "", + 0, + []*group.Vote{ + s.vote, + }, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.QueryVotesByVoterCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + + var res group.QueryVotesByVoterResponse + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &res)) + s.Require().Equal(len(res.Votes), len(tc.expectVotes)) + for i := range res.Votes { + s.Require().Equal(res.Votes[i], tc.expectVotes[i]) + } + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxCreateGroup() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + validMembers := fmt.Sprintf(`[{ + "address": "%s", + "weight": "1", + "metadata": "%s" + }]`, val.Address.String(), validMetadata) + validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) + + invalidMembersAddress := `[{ + "address": "", + "weight": "1" +}]` + invalidMembersAddressFile := testutil.WriteToNewTempFile(s.T(), invalidMembersAddress) + + invalidMembersWeight := fmt.Sprintf(`[{ + "address": "%s", + "weight": "0" + }]`, val.Address.String()) + invalidMembersWeightFile := testutil.WriteToNewTempFile(s.T(), invalidMembersWeight) + + invalidMembersMetadata := fmt.Sprintf(`[{ + "address": "%s", + "weight": "1", + "metadata": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==" + }]`, val.Address.String()) + invalidMembersMetadataFile := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata) + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + val.Address.String(), + "", + validMembersFile.Name(), + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "group metadata too long", + append( + []string{ + val.Address.String(), + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==", + "", + }, + commonFlags..., + ), + true, + "group metadata: limit exceeded", + nil, + 0, + }, + { + "invalid members address", + append( + []string{ + val.Address.String(), + "null", + invalidMembersAddressFile.Name(), + }, + commonFlags..., + ), + true, + "message validation failed: members: address: empty address string is not allowed", + nil, + 0, + }, + { + "invalid members weight", + append( + []string{ + val.Address.String(), + "null", + invalidMembersWeightFile.Name(), + }, + commonFlags..., + ), + true, + "message validation failed: member weight: expected a positive decimal, got 0", + nil, + 0, + }, + { + "members metadata too long", + append( + []string{ + val.Address.String(), + "null", + invalidMembersMetadataFile.Name(), + }, + commonFlags..., + ), + true, + "member metadata: limit exceeded", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgCreateGroupCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupAdmin() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + validMembers := fmt.Sprintf(`[{ + "address": "%s", + "weight": "1", + "metadata": "%s" + }]`, val.Address.String(), validMetadata) + validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) + out, err := cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateGroupCmd(), + append( + []string{ + val.Address.String(), + validMetadata, + validMembersFile.Name(), + }, + commonFlags..., + ), + ) + + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + val.Address.String(), + "3", + s.network.Validators[1].Address.String(), + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "group id invalid", + append( + []string{ + val.Address.String(), + "", + s.network.Validators[1].Address.String(), + }, + commonFlags..., + ), + true, + "strconv.ParseUint: parsing \"\": invalid syntax", + nil, + 0, + }, + { + "group doesn't exist", + append( + []string{ + val.Address.String(), + "12345", + s.network.Validators[1].Address.String(), + }, + commonFlags..., + ), + true, + "not found", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupAdminCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupMetadata() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + val.Address.String(), + "2", + validMetadata, + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "group metadata too long", + append( + []string{ + val.Address.String(), + strconv.FormatUint(s.group.GroupId.Uint64(), 10), + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==", + }, + commonFlags..., + ), + true, + "group metadata: limit exceeded", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupMetadataCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupMembers() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + validUpdatedMembersFileName := testutil.WriteToNewTempFile(s.T(), fmt.Sprintf(`[{ + "address": "%s", + "weight": "0", + "metadata": "%s" + }, { + "address": "%s", + "weight": "1", + "metadata": "%s" + }]`, val.Address.String(), validMetadata, s.groupAccount.GroupAccount, validMetadata)).Name() + + invalidMembersMetadata := fmt.Sprintf(`[{ + "address": "%s", + "weight": "1", + "metadata": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==" + }]`, val.Address.String()) + invalidMembersMetadataFileName := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata).Name() + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + val.Address.String(), + "2", + validUpdatedMembersFileName, + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "group member metadata too long", + append( + []string{ + val.Address.String(), + strconv.FormatUint(s.group.GroupId.Uint64(), 10), + invalidMembersMetadataFileName, + }, + commonFlags..., + ), + true, + "group member metadata: limit exceeded", + nil, + 0, + }, + { + "group doesn't exist", + append( + []string{ + val.Address.String(), + "12345", + validUpdatedMembersFileName, + }, + commonFlags..., + ), + true, + "not found", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupMembersCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxCreateGroupAccount() { + val := s.network.Validators[0] + wrongAdmin := s.network.Validators[1].Address + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + groupID := s.group.GroupId + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + val.Address.String(), + fmt.Sprintf("%v", groupID), + validMetadata, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "wrong admin", + append( + []string{ + wrongAdmin.String(), + fmt.Sprintf("%v", groupID), + validMetadata, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + true, + "The specified item could not be found in the keyring", + &sdk.TxResponse{}, + 0, + }, + { + "metadata too long", + append( + []string{ + val.Address.String(), + fmt.Sprintf("%v", groupID), + strings.Repeat("a", 500), + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + true, + "group account metadata: limit exceeded", + &sdk.TxResponse{}, + 0, + }, + { + "wrong group id", + append( + []string{ + val.Address.String(), + "10", + validMetadata, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + true, + "not found", + &sdk.TxResponse{}, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgCreateGroupAccountCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupAccountAdmin() { + val := s.network.Validators[0] + newAdmin := s.network.Validators[1].Address + clientCtx := val.ClientCtx + groupAccount := s.groupAccount2 + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + groupAccount.Admin, + groupAccount.GroupAccount, + newAdmin.String(), + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "wrong admin", + append( + []string{ + newAdmin.String(), + groupAccount.GroupAccount, + newAdmin.String(), + }, + commonFlags..., + ), + true, + "The specified item could not be found in the keyring", + &sdk.TxResponse{}, + 0, + }, + { + "wrong group account", + append( + []string{ + groupAccount.Admin, + newAdmin.String(), + newAdmin.String(), + }, + commonFlags..., + ), + true, + "load group account: not found", + &sdk.TxResponse{}, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupAccountAdminCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupAccountDecisionPolicy() { + val := s.network.Validators[0] + newAdmin := s.network.Validators[1].Address + clientCtx := val.ClientCtx + groupAccount := s.groupAccount3 + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + groupAccount.Admin, + groupAccount.GroupAccount, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"2\", \"timeout\":\"2s\"}", + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "wrong admin", + append( + []string{ + newAdmin.String(), + groupAccount.GroupAccount, + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + true, + "The specified item could not be found in the keyring", + &sdk.TxResponse{}, + 0, + }, + { + "wrong group account", + append( + []string{ + groupAccount.Admin, + newAdmin.String(), + "{\"@type\":\"/regen.group.v1alpha1.ThresholdDecisionPolicy\", \"threshold\":\"1\", \"timeout\":\"1s\"}", + }, + commonFlags..., + ), + true, + "load group account: not found", + &sdk.TxResponse{}, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupAccountDecisionPolicyCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxUpdateGroupAccountMetadata() { + val := s.network.Validators[0] + newAdmin := s.network.Validators[1].Address + clientCtx := val.ClientCtx + groupAccount := s.groupAccount3 + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + groupAccount.Admin, + groupAccount.GroupAccount, + validMetadata, + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "long metadata", + append( + []string{ + groupAccount.Admin, + groupAccount.GroupAccount, + strings.Repeat("a", 500), + }, + commonFlags..., + ), + true, + "group account metadata: limit exceeded", + &sdk.TxResponse{}, + 0, + }, + { + "wrong admin", + append( + []string{ + newAdmin.String(), + groupAccount.GroupAccount, + validMetadata, + }, + commonFlags..., + ), + true, + "The specified item could not be found in the keyring", + &sdk.TxResponse{}, + 0, + }, + { + "wrong group account", + append( + []string{ + groupAccount.Admin, + newAdmin.String(), + validMetadata, + }, + commonFlags..., + ), + true, + "load group account: not found", + &sdk.TxResponse{}, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgUpdateGroupAccountMetadataCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxCreateProposal() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + validTxFileName := getTxSendFileName(s, s.groupAccount.GroupAccount, val.Address.String()) + unauthzTxFileName := getTxSendFileName(s, val.Address.String(), s.groupAccount.GroupAccount) + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "metadata too long", + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + validTxFileName, + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "metadata: limit exceeded", + nil, + 0, + }, + { + "unauthorized msg", + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + unauthzTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "msg does not have group account authorization: unauthorized", + nil, + 0, + }, + { + "invalid proposers", + append( + []string{ + s.groupAccount.GroupAccount, + "invalid", + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "proposers: decoding bech32 failed", + nil, + 0, + }, + { + "invalid group account", + append( + []string{ + "invalid", + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "group account: decoding bech32 failed", + nil, + 0, + }, + { + "no group account", + append( + []string{ + val.Address.String(), + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "group account: not found", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgCreateProposalCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxVote() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + validTxFileName := getTxSendFileName(s, s.groupAccount.GroupAccount, val.Address.String()) + out, err := cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateProposalCmd(), + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + "2", + val.Address.String(), + "CHOICE_YES", + "", + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "invalid proposal id", + append( + []string{ + "abcd", + val.Address.String(), + "CHOICE_YES", + "", + }, + commonFlags..., + ), + true, + "invalid syntax", + nil, + 0, + }, + { + "proposal not found", + append( + []string{ + "1234", + val.Address.String(), + "CHOICE_YES", + "", + }, + commonFlags..., + ), + true, + "proposal: not found", + nil, + 0, + }, + { + "metadata too long", + append( + []string{ + "2", + val.Address.String(), + "CHOICE_YES", + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==", + }, + commonFlags..., + ), + true, + "metadata: limit exceeded", + nil, + 0, + }, + { + "invalid choice", + append( + []string{ + "2", + val.Address.String(), + "INVALID_CHOICE", + "", + }, + commonFlags..., + ), + true, + "not a valid vote choice", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgVoteCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func (s *IntegrationTestSuite) TestTxExec() { + val := s.network.Validators[0] + clientCtx := val.ClientCtx + + var commonFlags = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + // create proposal + validTxFileName := getTxSendFileName(s, s.groupAccount.GroupAccount, val.Address.String()) + out, err := cli.ExecTestCLICmd(val.ClientCtx, client.MsgCreateProposalCmd(), + append( + []string{ + s.groupAccount.GroupAccount, + val.Address.String(), + validTxFileName, + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + + // vote + out, err = cli.ExecTestCLICmd(val.ClientCtx, client.MsgVoteCmd(), + append( + []string{ + "3", + val.Address.String(), + "CHOICE_YES", + "", + }, + commonFlags..., + ), + ) + s.Require().NoError(err, out.String()) + + testCases := []struct { + name string + args []string + expectErr bool + expectErrMsg string + respType proto.Message + expectedCode uint32 + }{ + { + "correct data", + append( + []string{ + "3", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + false, + "", + &sdk.TxResponse{}, + 0, + }, + { + "invalid proposal id", + append( + []string{ + "abcd", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "invalid syntax", + nil, + 0, + }, + { + "proposal not found", + append( + []string{ + "1234", + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + }, + commonFlags..., + ), + true, + "proposal: not found", + nil, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + + s.Run(tc.name, func() { + cmd := client.MsgExecCmd() + + out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErr { + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err, out.String()) + s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) + } + }) + } +} + +func getTxSendFileName(s *IntegrationTestSuite, from string, to string) string { + tx := fmt.Sprintf( + `{"body":{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgSend","from_address":"%s","to_address":"%s","amount":[{"denom":"%s","amount":"10"}]}],"memo":"","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[],"fee":{"amount":[],"gas_limit":"200000","payer":"","granter":""}},"signatures":[]}`, + from, to, s.cfg.BondDenom, + ) + return testutil.WriteToNewTempFile(s.T(), tx).Name() +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/x/group/client/tx.go b/x/group/client/tx.go new file mode 100644 index 0000000000..458ba59f10 --- /dev/null +++ b/x/group/client/tx.go @@ -0,0 +1,637 @@ +package client + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + + "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" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/version" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" + + "github.com/regen-network/regen-ledger/x/group" + "github.com/spf13/cobra" +) + +const flagMembers = "members" + +// TxCmd returns a root CLI command handler for all x/group transaction commands. +func TxCmd(name string) *cobra.Command { + txCmd := &cobra.Command{ + Use: name, + Short: "Group transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + txCmd.AddCommand( + MsgCreateGroupCmd(), + MsgUpdateGroupAdminCmd(), + MsgUpdateGroupMetadataCmd(), + MsgUpdateGroupMembersCmd(), + MsgCreateGroupAccountCmd(), + MsgUpdateGroupAccountAdminCmd(), + MsgUpdateGroupAccountDecisionPolicyCmd(), + MsgUpdateGroupAccountMetadataCmd(), + MsgCreateProposalCmd(), + MsgVoteCmd(), + MsgExecCmd(), + ) + + return txCmd +} + +// MsgCreateGroupCmd creates a CLI command for Msg/CreateGroup. +func MsgCreateGroupCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-group [admin] [metadata] [members-json-file]", + Short: "Create a group which is an aggregation " + + "of member accounts with associated weights and " + + "an administrator account. Note, the '--from' flag is " + + "ignored as it is implied from [admin].", + Long: strings.TrimSpace( + fmt.Sprintf(`Create a group which is an aggregation of member accounts with associated weights and +an administrator account. Note, the '--from' flag is ignored as it is implied from [admin]. +Members accounts can be given through a members JSON file that contains an array of members. + +Example: +$ %s tx group create-group [admin] [metadata] [members-json-file] + +Where members.json contains: + +[ + { + "address": "addr1", + "weight": "1", + "metadata": "some metadata" + }, + { + "address": "addr2", + "weight": "1", + "metadata": "some metadata" + } +] +`, + version.AppName, + ), + ), + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + members, err := parseMembers(args[2]) + if err != nil { + return err + } + + b, err := base64.StdEncoding.DecodeString(args[1]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg := &group.MsgCreateGroupRequest{ + Admin: clientCtx.GetFromAddress().String(), + Members: members, + Metadata: b, + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + cmd.Flags().String(flagMembers, "", "Members file path") + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupMembersCmd creates a CLI command for Msg/UpdateGroupMembers. +func MsgUpdateGroupMembersCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-members [admin] [group-id] [members-json-file]", + Short: "Update a group's members. Set a member's weight to \"0\" to delete it.", + Long: strings.TrimSpace( + fmt.Sprintf(`Update a group's members + +Example: +$ %s tx group update-group-members [admin] [group-id] [members-json-file] + +Where members.json contains: + +[ + { + "address": "addr1", + "weight": "1", + "metadata": "some new metadata" + }, + { + "address": "addr2", + "weight": "0", + "metadata": "some metadata" + } +] + +Set a member's weight to "0" to delete it. +`, + version.AppName, + ), + ), + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + members, err := parseMembers(args[2]) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + msg := &group.MsgUpdateGroupMembersRequest{ + Admin: clientCtx.GetFromAddress().String(), + MemberUpdates: members, + GroupId: group.ID(groupID), + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + cmd.Flags().String(flagMembers, "", "Members file path") + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupAdminCmd creates a CLI command for Msg/UpdateGroupAdmin. +func MsgUpdateGroupAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-admin [admin] [group-id] [new-admin]", + Short: "Update a group's admin", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + msg := &group.MsgUpdateGroupAdminRequest{ + Admin: clientCtx.GetFromAddress().String(), + NewAdmin: args[2], + GroupId: group.ID(groupID), + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupMetadataCmd creates a CLI command for Msg/UpdateGroupMetadata. +func MsgUpdateGroupMetadataCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-admin [admin] [group-id] [metadata]", + Short: "Update a group's admin", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + b, err := base64.StdEncoding.DecodeString(args[2]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg := &group.MsgUpdateGroupMetadataRequest{ + Admin: clientCtx.GetFromAddress().String(), + Metadata: b, + GroupId: group.ID(groupID), + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgCreateGroupAccountCmd creates a CLI command for Msg/CreateGroupAccount. +func MsgCreateGroupAccountCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-group-account [admin] [group-id] [metadata] [decision-policy]", + Short: "Create a group account which is an account " + + "associated with a group and a decision policy. " + + "Note, the '--from' flag is " + + "ignored as it is implied from [admin].", + Long: strings.TrimSpace( + fmt.Sprintf(`Create a group account which is an account associated with a group and a decision policy. +Note, the '--from' flag is ignored as it is implied from [admin]. + +Example: +$ %s tx group create-group-account [admin] [group-id] [metadata] \ +'{"@type":"/regen.group.v1alpha1.ThresholdDecisionPolicy", "threshold":"1", "timeout":"1s"}' +`, + version.AppName, + ), + ), + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + groupID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + var policy group.DecisionPolicy + if err := clientCtx.JSONMarshaler.UnmarshalInterfaceJSON([]byte(args[3]), &policy); err != nil { + return err + } + + b, err := base64.StdEncoding.DecodeString(args[2]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg, err := group.NewMsgCreateGroupAccountRequest( + clientCtx.GetFromAddress(), + group.ID(groupID), + b, + policy, + ) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupAccountAdminCmd creates a CLI command for Msg/UpdateGroupAccountAdmin. +func MsgUpdateGroupAccountAdminCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-account-admin [admin] [group-account] [new-admin]", + Short: "Update a group account admin", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := &group.MsgUpdateGroupAccountAdminRequest{ + Admin: clientCtx.GetFromAddress().String(), + GroupAccount: args[1], + NewAdmin: args[2], + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupAccountDecisionPolicyCmd creates a CLI command for Msg/UpdateGroupAccountDecisionPolicy. +func MsgUpdateGroupAccountDecisionPolicyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-account-policy [admin] [group-account] [decision-policy]", + Short: "Update a group account decision policy", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + var policy group.DecisionPolicy + if err := clientCtx.JSONMarshaler.UnmarshalInterfaceJSON([]byte(args[2]), &policy); err != nil { + return err + } + + accountAddress, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + msg, err := group.NewMsgUpdateGroupAccountDecisionPolicyRequest( + clientCtx.GetFromAddress(), + accountAddress, + policy, + ) + if err != nil { + return err + } + + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgUpdateGroupAccountMetadataCmd creates a CLI command for Msg/MsgUpdateGroupAccountMetadata. +func MsgUpdateGroupAccountMetadataCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update-group-account-metadata [admin] [group-account] [new-metadata]", + Short: "Update a group account metadata", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[0]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + b, err := base64.StdEncoding.DecodeString(args[2]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg := &group.MsgUpdateGroupAccountMetadataRequest{ + Admin: clientCtx.GetFromAddress().String(), + GroupAccount: args[1], + Metadata: b, + } + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgCreateProposalCmd creates a CLI command for Msg/CreateProposal. +func MsgCreateProposalCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-proposal [group-account] [proposer[,proposer]*] [msg_tx_json_file] [metadata]", + Short: "Submit a new proposal", + Long: `Submit a new proposal. + +Parameters: + group-account: address of the group account + proposer: comma separated (no spaces) list of proposer account addresses. Example: "addr1,addr2" + Metadata: metadata for the proposal + msg_tx_json_file: path to json file with messages that will be executed if the proposal is accepted. +`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + proposers := strings.Split(args[1], ",") + for i := range proposers { + proposers[i] = strings.TrimSpace(proposers[i]) + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + theTx, err := authclient.ReadTxFromFile(clientCtx, args[2]) + if err != nil { + return err + } + msgs := theTx.GetMsgs() + + b, err := base64.StdEncoding.DecodeString(args[3]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg, err := group.NewMsgCreateProposalRequest( + args[0], + proposers, + msgs, + b, + ) + if err != nil { + return err + } + + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgVoteCmd creates a CLI command for Msg/Vote. +func MsgVoteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "vote [proposal-id] [voter] [choice] [metadata]", + Short: "Vote on a proposal", + Long: `Vote on a proposal. + +Parameters: + proposal-id: unique ID of the proposal + voter: voter account addresses. + choice: choice of the voter(s) + CHOICE_UNSPECIFIED: no-op + CHOICE_NO: no + CHOICE_YES: yes + CHOICE_ABSTAIN: abstain + CHOICE_VETO: veto + Metadata: metadata for the vote +`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + err := cmd.Flags().Set(flags.FlagFrom, args[1]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + choice, err := group.ChoiceFromString(args[2]) + if err != nil { + return err + } + + b, err := base64.StdEncoding.DecodeString(args[3]) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "metadata is malformed, proper base64 string is required") + } + + msg := &group.MsgVoteRequest{ + ProposalId: group.ProposalID(proposalID), + Voter: args[1], + Choice: choice, + Metadata: b, + } + if err != nil { + return err + } + + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// MsgExecCmd creates a CLI command for Msg/MsgExec. +func MsgExecCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "exec [proposal-id]", + Short: "Execute a proposal", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + msg := &group.MsgExecRequest{ + ProposalId: group.ProposalID(proposalID), + Signer: clientCtx.GetFromAddress().String(), + } + if err != nil { + return err + } + + if err = msg.ValidateBasic(); err != nil { + return fmt.Errorf("message validation failed: %w", err) + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/group/client/util.go b/x/group/client/util.go new file mode 100644 index 0000000000..04083ea7a9 --- /dev/null +++ b/x/group/client/util.go @@ -0,0 +1,28 @@ +package client + +import ( + "encoding/json" + "io/ioutil" + + "github.com/regen-network/regen-ledger/x/group" +) + +func parseMembers(membersFile string) ([]group.Member, error) { + members := []group.Member{} + + if membersFile == "" { + return members, nil + } + + contents, err := ioutil.ReadFile(membersFile) + if err != nil { + return nil, err + } + + err = json.Unmarshal(contents, &members) + if err != nil { + return nil, err + } + + return members, nil +} diff --git a/x/group/codec.go b/x/group/codec.go index e07d9362ab..75be45627f 100644 --- a/x/group/codec.go +++ b/x/group/codec.go @@ -1,11 +1,47 @@ package group import ( + "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/msgservice" ) +// RegisterLegacyAminoCodec registers all the necessary group module concrete +// types and interfaces with the provided codec reference. +// These types are used for Amino JSON serialization. +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterInterface((*DecisionPolicy)(nil), nil) + cdc.RegisterConcrete(&ThresholdDecisionPolicy{}, "cosmos-sdk/ThresholdDecisionPolicy", nil) + cdc.RegisterConcrete(&MsgCreateGroupRequest{}, "cosmos-sdk/MsgCreateGroup", nil) + cdc.RegisterConcrete(&MsgUpdateGroupMembersRequest{}, "cosmos-sdk/MsgUpdateGroupMembers", nil) + cdc.RegisterConcrete(&MsgUpdateGroupAdminRequest{}, "cosmos-sdk/MsgUpdateGroupAdmin", nil) + cdc.RegisterConcrete(&MsgUpdateGroupMetadataRequest{}, "cosmos-sdk/MsgUpdateGroupMetadata", nil) + cdc.RegisterConcrete(&MsgCreateGroupAccountRequest{}, "cosmos-sdk/MsgCreateGroupAccount", nil) + cdc.RegisterConcrete(&MsgUpdateGroupAccountAdminRequest{}, "cosmos-sdk/MsgUpdateGroupAccountAdmin", nil) + cdc.RegisterConcrete(&MsgUpdateGroupAccountDecisionPolicyRequest{}, "cosmos-sdk/MsgUpdateGroupAccountDecisionPolicy", nil) + cdc.RegisterConcrete(&MsgUpdateGroupAccountMetadataRequest{}, "cosmos-sdk/MsgUpdateGroupAccountMetadata", nil) + cdc.RegisterConcrete(&MsgCreateProposalRequest{}, "cosmos-sdk/group/MsgCreateProposal", nil) + cdc.RegisterConcrete(&MsgVoteRequest{}, "cosmos-sdk/group/MsgVote", nil) + cdc.RegisterConcrete(&MsgExecRequest{}, "cosmos-sdk/group/MsgExec", nil) +} + func RegisterTypes(registry cdctypes.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgCreateGroupRequest{}, + &MsgUpdateGroupMembersRequest{}, + &MsgUpdateGroupAdminRequest{}, + &MsgUpdateGroupMetadataRequest{}, + &MsgCreateGroupAccountRequest{}, + &MsgUpdateGroupAccountAdminRequest{}, + &MsgUpdateGroupAccountDecisionPolicyRequest{}, + &MsgUpdateGroupAccountMetadataRequest{}, + &MsgCreateProposalRequest{}, + &MsgVoteRequest{}, + &MsgExecRequest{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &Msg_ServiceDesc) registry.RegisterInterface( @@ -14,3 +50,13 @@ func RegisterTypes(registry cdctypes.InterfaceRegistry) { &ThresholdDecisionPolicy{}, ) } + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewAminoCodec(amino) +) + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) +} diff --git a/x/group/keys.go b/x/group/keys.go index 1aed1a30de..4086423f60 100644 --- a/x/group/keys.go +++ b/x/group/keys.go @@ -8,4 +8,7 @@ const ( StoreKey = ModuleName DefaultParamspace = ModuleName + + // RouterKey defines the module's message routing key + RouterKey = ModuleName ) diff --git a/x/group/module/module.go b/x/group/module/module.go index 8c1229df67..df7ab72532 100644 --- a/x/group/module/module.go +++ b/x/group/module/module.go @@ -8,11 +8,13 @@ import ( climodule "github.com/regen-network/regen-ledger/types/module/client/cli" servermodule "github.com/regen-network/regen-ledger/types/module/server" "github.com/regen-network/regen-ledger/x/group" + "github.com/regen-network/regen-ledger/x/group/client" "github.com/regen-network/regen-ledger/x/group/server" - "github.com/cosmos/cosmos-sdk/client" + sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/gorilla/mux" "github.com/spf13/cobra" @@ -25,6 +27,7 @@ type Module struct { var _ module.AppModuleBasic = Module{} var _ servermodule.Module = Module{} var _ climodule.Module = Module{} +var _ servermodule.LegacyRouteModule = Module{} func (a Module) Name() string { return group.ModuleName @@ -43,7 +46,7 @@ func (a Module) DefaultGenesis(marshaler codec.JSONMarshaler) json.RawMessage { return marshaler.MustMarshalJSON(group.NewGenesisState()) } -func (a Module) ValidateGenesis(cdc codec.JSONMarshaler, config client.TxEncodingConfig, bz json.RawMessage) error { +func (a Module) ValidateGenesis(cdc codec.JSONMarshaler, config sdkclient.TxEncodingConfig, bz json.RawMessage) error { var data group.GenesisState if err := cdc.UnmarshalJSON(bz, &data); err != nil { return fmt.Errorf("failed to unmarshal %s genesis state: %w", group.ModuleName, err) @@ -51,18 +54,22 @@ func (a Module) ValidateGenesis(cdc codec.JSONMarshaler, config client.TxEncodin return data.Validate() } -func (a Module) RegisterGRPCGatewayRoutes(client.Context, *runtime.ServeMux) {} +func (a Module) RegisterGRPCGatewayRoutes(sdkclient.Context, *runtime.ServeMux) {} func (a Module) GetTxCmd() *cobra.Command { - // TODO #209 - return nil + return client.TxCmd(a.Name()) } func (a Module) GetQueryCmd() *cobra.Command { - // TODO #209 - return nil + return client.QueryCmd(a.Name()) } /**** DEPRECATED ****/ -func (a Module) RegisterRESTRoutes(client.Context, *mux.Router) {} -func (a Module) RegisterLegacyAminoCodec(*codec.LegacyAmino) {} +func (a Module) RegisterRESTRoutes(sdkclient.Context, *mux.Router) {} +func (a Module) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + group.RegisterLegacyAminoCodec(cdc) +} + +func (a Module) Route(configurator servermodule.Configurator) sdk.Route { + return sdk.NewRoute(group.RouterKey, server.NewHandler(configurator, a.AccountKeeper)) +} diff --git a/x/group/msgs.go b/x/group/msgs.go index d04b45947d..7231ac4ea3 100644 --- a/x/group/msgs.go +++ b/x/group/msgs.go @@ -10,7 +10,33 @@ import ( "github.com/regen-network/regen-ledger/math" ) -var _ sdk.MsgRequest = &MsgCreateGroupRequest{} +// Group message types and routes +const ( + TypeMsgCreateGroup = "create_group" + TypeMsgUpdateGroupAdmin = "update_group_admin" + TypeMsgUpdateGroupComment = "update_group_comment" + TypeMsgUpdateGroupMembers = "update_group_members" + TypeMsgCreateGroupAccount = "create_group_account" + TypeMsgUpdateGroupAccountAdmin = "update_group_account_admin" + TypeMsgUpdateGroupAccountDecisionPolicy = "update_group_account_decision_policy" + TypeMsgUpdateGroupAccountComment = "update_group_account_comment" + TypeMsgCreateProposal = "create_proposal" + TypeMsgVote = "vote" + TypeMsgExec = "exec" +) + +var _ sdk.Msg = &MsgCreateGroupRequest{} + +// Route Implements Msg. +func (m MsgCreateGroupRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgCreateGroupRequest) Type() string { return TypeMsgCreateGroup } + +// GetSignBytes Implements Msg. +func (m MsgCreateGroupRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgCreateGroupRequest. func (m MsgCreateGroupRequest) GetSigners() []sdk.AccAddress { @@ -52,7 +78,18 @@ func (m Member) ValidateBasic() error { return nil } -var _ sdk.MsgRequest = &MsgUpdateGroupAdminRequest{} +var _ sdk.Msg = &MsgUpdateGroupAdminRequest{} + +// Route Implements Msg. +func (m MsgUpdateGroupAdminRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupAdminRequest) Type() string { return TypeMsgUpdateGroupAdmin } + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupAdminRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgUpdateGroupAdminRequest. func (m MsgUpdateGroupAdminRequest) GetSigners() []sdk.AccAddress { @@ -89,7 +126,18 @@ func (m *MsgUpdateGroupAdminRequest) GetGroupID() ID { return m.GroupId } -var _ sdk.MsgRequest = &MsgUpdateGroupMetadataRequest{} +var _ sdk.Msg = &MsgUpdateGroupMetadataRequest{} + +// Route Implements Msg. +func (m MsgUpdateGroupMetadataRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupMetadataRequest) Type() string { return TypeMsgUpdateGroupComment } + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupMetadataRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgUpdateGroupMetadataRequest. func (m MsgUpdateGroupMetadataRequest) GetSigners() []sdk.AccAddress { @@ -117,6 +165,19 @@ func (m *MsgUpdateGroupMetadataRequest) GetGroupID() ID { return m.GroupId } +var _ sdk.Msg = &MsgUpdateGroupMembersRequest{} + +// Route Implements Msg. +func (m MsgUpdateGroupMembersRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupMembersRequest) Type() string { return TypeMsgUpdateGroupMembers } + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupMembersRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + var _ sdk.MsgRequest = &MsgUpdateGroupMembersRequest{} // GetSigners returns the expected signers for a MsgUpdateGroupMembersRequest. @@ -152,7 +213,18 @@ func (m *MsgUpdateGroupMembersRequest) GetGroupID() ID { return m.GroupId } -var _ sdk.MsgRequest = &MsgCreateGroupAccountRequest{} +var _ sdk.Msg = &MsgCreateGroupAccountRequest{} + +// Route Implements Msg. +func (m MsgCreateGroupAccountRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgCreateGroupAccountRequest) Type() string { return TypeMsgCreateGroupAccount } + +// GetSignBytes Implements Msg. +func (m MsgCreateGroupAccountRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgCreateGroupAccountRequest. func (m MsgCreateGroupAccountRequest) GetSigners() []sdk.AccAddress { @@ -184,7 +256,18 @@ func (m MsgCreateGroupAccountRequest) ValidateBasic() error { return nil } -var _ sdk.MsgRequest = &MsgUpdateGroupAccountAdminRequest{} +var _ sdk.Msg = &MsgUpdateGroupAccountAdminRequest{} + +// Route Implements Msg. +func (m MsgUpdateGroupAccountAdminRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupAccountAdminRequest) Type() string { return TypeMsgUpdateGroupAccountAdmin } + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupAccountAdminRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgUpdateGroupAccountAdminRequest. func (m MsgUpdateGroupAccountAdminRequest) GetSigners() []sdk.AccAddress { @@ -218,7 +301,7 @@ func (m MsgUpdateGroupAccountAdminRequest) ValidateBasic() error { return nil } -var _ sdk.MsgRequest = &MsgUpdateGroupAccountDecisionPolicyRequest{} +var _ sdk.Msg = &MsgUpdateGroupAccountDecisionPolicyRequest{} var _ types.UnpackInterfacesMessage = MsgUpdateGroupAccountDecisionPolicyRequest{} func NewMsgUpdateGroupAccountDecisionPolicyRequest(admin sdk.AccAddress, groupAccount sdk.AccAddress, decisionPolicy DecisionPolicy) (*MsgUpdateGroupAccountDecisionPolicyRequest, error) { @@ -246,6 +329,19 @@ func (m *MsgUpdateGroupAccountDecisionPolicyRequest) SetDecisionPolicy(decisionP return nil } +// Route Implements Msg. +func (m MsgUpdateGroupAccountDecisionPolicyRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupAccountDecisionPolicyRequest) Type() string { + return TypeMsgUpdateGroupAccountDecisionPolicy +} + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupAccountDecisionPolicyRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + // GetSigners returns the expected signers for a MsgUpdateGroupAccountDecisionPolicyRequest. func (m MsgUpdateGroupAccountDecisionPolicyRequest) GetSigners() []sdk.AccAddress { admin, err := sdk.AccAddressFromBech32(m.Admin) @@ -293,7 +389,18 @@ func (m MsgUpdateGroupAccountDecisionPolicyRequest) UnpackInterfaces(unpacker ty return unpacker.UnpackAny(m.DecisionPolicy, &decisionPolicy) } -var _ sdk.MsgRequest = &MsgUpdateGroupAccountMetadataRequest{} +var _ sdk.Msg = &MsgUpdateGroupAccountMetadataRequest{} + +// Route Implements Msg. +func (m MsgUpdateGroupAccountMetadataRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgUpdateGroupAccountMetadataRequest) Type() string { return TypeMsgUpdateGroupAccountComment } + +// GetSignBytes Implements Msg. +func (m MsgUpdateGroupAccountMetadataRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgUpdateGroupAccountMetadataRequest. func (m MsgUpdateGroupAccountMetadataRequest) GetSigners() []sdk.AccAddress { @@ -319,7 +426,7 @@ func (m MsgUpdateGroupAccountMetadataRequest) ValidateBasic() error { return nil } -var _ sdk.MsgRequest = &MsgCreateGroupAccountRequest{} +var _ sdk.Msg = &MsgCreateGroupAccountRequest{} var _ types.UnpackInterfacesMessage = MsgCreateGroupAccountRequest{} // NewMsgCreateGroupAccountRequest creates a new MsgCreateGroupAccountRequest. @@ -375,7 +482,32 @@ func (m MsgCreateGroupAccountRequest) UnpackInterfaces(unpacker types.AnyUnpacke return unpacker.UnpackAny(m.DecisionPolicy, &decisionPolicy) } -var _ sdk.MsgRequest = &MsgCreateProposalRequest{} +var _ sdk.Msg = &MsgCreateProposalRequest{} + +// NewMsgCreateProposalRequest creates a new MsgCreateProposalRequest. +func NewMsgCreateProposalRequest(acc string, proposers []string, msgs []sdk.Msg, metadata []byte) (*MsgCreateProposalRequest, error) { + m := &MsgCreateProposalRequest{ + GroupAccount: acc, + Proposers: proposers, + Metadata: metadata, + } + err := m.SetMsgs(msgs) + if err != nil { + return nil, err + } + return m, nil +} + +// Route Implements Msg. +func (m MsgCreateProposalRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgCreateProposalRequest) Type() string { return TypeMsgCreateProposal } + +// GetSignBytes Implements Msg. +func (m MsgCreateProposalRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgCreateProposalRequest. func (m MsgCreateProposalRequest) GetSigners() []sdk.AccAddress { @@ -412,11 +544,8 @@ func (m MsgCreateProposalRequest) ValidateBasic() error { return sdkerrors.Wrap(err, "proposers") } - for i, any := range m.Msgs { - msg, ok := any.GetCachedValue().(sdk.Msg) - if !ok { - return sdkerrors.Wrapf(sdkerrors.ErrUnpackAny, "cannot unpack Any into sdk.Msg %T", any) - } + msgs := m.GetMsgs() + for i, msg := range msgs { if err := msg.ValidateBasic(); err != nil { return sdkerrors.Wrapf(err, "msg %d", i) } @@ -454,8 +583,15 @@ func (m MsgCreateProposalRequest) GetMsgs() []sdk.Msg { // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces func (m MsgCreateProposalRequest) UnpackInterfaces(unpacker types.AnyUnpacker) error { - for _, m := range m.Msgs { - err := types.UnpackInterfaces(m, unpacker) + // for _, m := range m.Msgs { + // err := types.UnpackInterfaces(m, unpacker) + // if err != nil { + // return err + // } + // } + for _, any := range m.Msgs { + var msg sdk.Msg + err := unpacker.UnpackAny(any, &msg) if err != nil { return err } @@ -464,7 +600,18 @@ func (m MsgCreateProposalRequest) UnpackInterfaces(unpacker types.AnyUnpacker) e return nil } -var _ sdk.MsgRequest = &MsgVoteRequest{} +var _ sdk.Msg = &MsgVoteRequest{} + +// Route Implements Msg. +func (m MsgVoteRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgVoteRequest) Type() string { return TypeMsgVote } + +// GetSignBytes Implements Msg. +func (m MsgVoteRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgVoteRequest. func (m MsgVoteRequest) GetSigners() []sdk.AccAddress { @@ -493,7 +640,18 @@ func (m MsgVoteRequest) ValidateBasic() error { return nil } -var _ sdk.MsgRequest = &MsgExecRequest{} +var _ sdk.Msg = &MsgExecRequest{} + +// Route Implements Msg. +func (m MsgExecRequest) Route() string { return RouterKey } + +// Type Implements Msg. +func (m MsgExecRequest) Type() string { return TypeMsgExec } + +// GetSignBytes Implements Msg. +func (m MsgExecRequest) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} // GetSigners returns the expected signers for a MsgExecRequest. func (m MsgExecRequest) GetSigners() []sdk.AccAddress { diff --git a/x/group/server/handler.go b/x/group/server/handler.go new file mode 100644 index 0000000000..a63f46dfad --- /dev/null +++ b/x/group/server/handler.go @@ -0,0 +1,69 @@ +package server + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/regen-network/regen-ledger/types" + servermodule "github.com/regen-network/regen-ledger/types/module/server" + "github.com/regen-network/regen-ledger/x/group" +) + +// NewHandler creates an sdk.Handler for all the group type messages. +// This is needed for supporting amino-json signing. +func NewHandler(configurator servermodule.Configurator, accountKeeper AccountKeeper) sdk.Handler { + impl := newServer(configurator.ModuleKey(), configurator.Router(), accountKeeper, configurator.Marshaler()) + + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + regenCtx := types.Context{Context: ctx} + + switch msg := msg.(type) { + case *group.MsgCreateGroupRequest: + res, err := impl.CreateGroup(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupMembersRequest: + res, err := impl.UpdateGroupMembers(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupAdminRequest: + res, err := impl.UpdateGroupAdmin(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupMetadataRequest: + res, err := impl.UpdateGroupMetadata(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgCreateGroupAccountRequest: + res, err := impl.CreateGroupAccount(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupAccountAdminRequest: + res, err := impl.UpdateGroupAccountAdmin(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupAccountDecisionPolicyRequest: + res, err := impl.UpdateGroupAccountDecisionPolicy(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgUpdateGroupAccountMetadataRequest: + res, err := impl.UpdateGroupAccountMetadata(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgCreateProposalRequest: + res, err := impl.CreateProposal(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgVoteRequest: + res, err := impl.Vote(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + case *group.MsgExecRequest: + res, err := impl.Exec(regenCtx, msg) + return sdk.WrapServiceResult(ctx, res, err) + + default: + return nil, errors.Wrapf(errors.ErrUnknownRequest, "unrecognized %s message type: %T", group.ModuleName, msg) + } + } +} diff --git a/x/group/server/msg_server.go b/x/group/server/msg_server.go index fc1a61f2e5..3273de714c 100644 --- a/x/group/server/msg_server.go +++ b/x/group/server/msg_server.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "fmt" "reflect" + "strconv" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -15,7 +16,6 @@ import ( "github.com/regen-network/regen-ledger/math" "github.com/regen-network/regen-ledger/orm" "github.com/regen-network/regen-ledger/types" - "github.com/regen-network/regen-ledger/util" "github.com/regen-network/regen-ledger/x/group" ) @@ -28,15 +28,14 @@ func (s serverImpl) CreateGroup(ctx types.Context, req *group.MsgCreateGroupRequ return nil, err } - maxMetadataLength := s.maxMetadataLength(ctx) - if err := assertMetadataLength(metadata, maxMetadataLength, "group metadata"); err != nil { + if err := assertMetadataLength(metadata, "group metadata"); err != nil { return nil, err } totalWeight := apd.New(0, 0) for i := range members { m := members[i] - if err := assertMetadataLength(m.Metadata, maxMetadataLength, "member metadata"); err != nil { + if err := assertMetadataLength(m.Metadata, "member metadata"); err != nil { return nil, err } @@ -82,8 +81,7 @@ func (s serverImpl) CreateGroup(ctx types.Context, req *group.MsgCreateGroupRequ } } - groupIDStr := util.Uint64ToBase58Check(groupID.Uint64()) - err = ctx.EventManager().EmitTypedEvent(&group.EventCreateGroup{GroupId: groupIDStr}) + err = ctx.EventManager().EmitTypedEvent(&group.EventCreateGroup{GroupId: strconv.FormatUint(groupID.Uint64(), 10)}) if err != nil { return nil, err } @@ -98,6 +96,9 @@ func (s serverImpl) UpdateGroupMembers(ctx types.Context, req *group.MsgUpdateGr return err } for i := range req.MemberUpdates { + if err := assertMetadataLength(req.MemberUpdates[i].Metadata, "group member metadata"); err != nil { + return err + } groupMember := group.GroupMember{GroupId: req.GroupId, Member: &group.Member{ Address: req.MemberUpdates[i].Address, @@ -208,6 +209,10 @@ func (s serverImpl) UpdateGroupMetadata(ctx types.Context, req *group.MsgUpdateG return s.groupTable.Save(ctx, g.GroupId.Bytes(), g) } + if err := assertMetadataLength(req.Metadata, "group metadata"); err != nil { + return nil, err + } + err := s.doUpdateGroup(ctx, req, action, "metadata updated") if err != nil { return nil, err @@ -225,7 +230,7 @@ func (s serverImpl) CreateGroupAccount(ctx types.Context, req *group.MsgCreateGr groupID := req.GetGroupID() metadata := req.GetMetadata() - if err := assertMetadataLength(metadata, s.maxMetadataLength(ctx), "group account metadata"); err != nil { + if err := assertMetadataLength(metadata, "group account metadata"); err != nil { return nil, err } @@ -341,7 +346,7 @@ func (s serverImpl) UpdateGroupAccountMetadata(ctx types.Context, req *group.Msg return s.groupAccountTable.Save(ctx, groupAccount) } - if err := assertMetadataLength(metadata, s.maxMetadataLength(ctx), "group account metadata"); err != nil { + if err := assertMetadataLength(metadata, "group account metadata"); err != nil { return nil, err } @@ -362,7 +367,7 @@ func (s serverImpl) CreateProposal(ctx types.Context, req *group.MsgCreatePropos proposers := req.Proposers msgs := req.GetMsgs() - if err := assertMetadataLength(metadata, s.maxMetadataLength(ctx), "metadata"); err != nil { + if err := assertMetadataLength(metadata, "metadata"); err != nil { return nil, err } @@ -453,7 +458,7 @@ func (s serverImpl) Vote(ctx types.Context, req *group.MsgVoteRequest) (*group.M choice := req.Choice metadata := req.Metadata - if err := assertMetadataLength(metadata, s.maxMetadataLength(ctx), "metadata"); err != nil { + if err := assertMetadataLength(metadata, "metadata"); err != nil { return nil, err } @@ -692,8 +697,7 @@ func (s serverImpl) doUpdateGroup(ctx types.Context, req authNGroupReq, action a return err } - groupIDStr := util.Uint64ToBase58Check(req.GetGroupID().Uint64()) - err = ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroup{GroupId: groupIDStr}) + err = ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroup{GroupId: strconv.FormatUint(req.GetGroupID().Uint64(), 10)}) if err != nil { return err } @@ -725,15 +729,10 @@ func (s serverImpl) doAuthenticated(ctx types.Context, req authNGroupReq, action return nil } -// maxMetadataLength returns the maximum length of a metadata field. -func (s serverImpl) maxMetadataLength(ctx types.Context) int { - return group.MaxMetadataLength -} - // assertMetadataLength returns an error if given metadata length // is greater than a fixed maxMetadataLength. -func assertMetadataLength(metadata []byte, maxMetadataLength int, description string) error { - if len(metadata) > maxMetadataLength { +func assertMetadataLength(metadata []byte, description string) error { + if len(metadata) > group.MaxMetadataLength { return sdkerrors.Wrap(group.ErrMaxLimit, description) } return nil diff --git a/x/group/server/testsuite/suite.go b/x/group/server/testsuite/suite.go index a32ddc4fa2..2c1c3c7046 100644 --- a/x/group/server/testsuite/suite.go +++ b/x/group/server/testsuite/suite.go @@ -163,7 +163,7 @@ func (s *IntegrationTestSuite) TestCreateGroup() { }, expGroups: expGroups, }, - "group comment too long": { + "group metadata too long": { req: &group.MsgCreateGroupRequest{ Admin: s.addr1.String(), Members: members, @@ -171,7 +171,7 @@ func (s *IntegrationTestSuite) TestCreateGroup() { }, expErr: true, }, - "member comment too long": { + "member metadata too long": { req: &group.MsgCreateGroupRequest{ Admin: s.addr1.String(), Members: []group.Member{{ @@ -751,7 +751,7 @@ func (s *IntegrationTestSuite) TestCreateGroupAccount() { ), expErr: true, }, - "comment too long": { + "metadata too long": { req: &group.MsgCreateGroupAccountRequest{ Admin: s.addr1.String(), Metadata: []byte(strings.Repeat("a", 256)), @@ -1148,14 +1148,14 @@ func (s *IntegrationTestSuite) TestCreateProposal() { Amount: sdk.Coins{sdk.NewInt64Coin("token", 100)}, }}, }, - // "comment too long": { - // req: &group.MsgCreateProposalRequest{ - // GroupAccount: accountAddr.String(), - // Metadata: strings.Repeat("a", 256), - // Proposers: []string{s.addr2.String()}, - // }, - // expErr: true, - // }, + "metadata too long": { + req: &group.MsgCreateProposalRequest{ + GroupAccount: accountAddr.String(), + Metadata: bytes.Repeat([]byte{1}, 256), + Proposers: []string{s.addr2.String()}, + }, + expErr: true, + }, "group account required": { req: &group.MsgCreateProposalRequest{ Metadata: nil, diff --git a/x/group/types.go b/x/group/types.go index 8ae26b3818..64ac5e54cf 100644 --- a/x/group/types.go +++ b/x/group/types.go @@ -268,6 +268,16 @@ func (v Vote) ValidateBasic() error { return nil } +// ChoiceFromString returns a Choice from a string. It returns an error +// if the string is invalid. +func ChoiceFromString(str string) (Choice, error) { + choice, ok := Choice_value[str] + if !ok { + return Choice_CHOICE_UNSPECIFIED, fmt.Errorf("'%s' is not a valid vote choice", str) + } + return Choice(choice), nil +} + // MaxMetadataLength defines the max length of the metadata bytes field // for various entities within the group module // TODO: This could be used as params once x/params is upgraded to use protobuf @@ -464,3 +474,27 @@ func (t Tally) ValidateBasic() error { } return nil } + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces +func (q QueryGroupAccountsByGroupResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for _, g := range q.GroupAccounts { + err := g.UnpackInterfaces(unpacker) + if err != nil { + return err + } + } + + return nil +} + +// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces +func (q QueryGroupAccountsByAdminResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for _, g := range q.GroupAccounts { + err := g.UnpackInterfaces(unpacker) + if err != nil { + return err + } + } + + return nil +}