Skip to content

Commit

Permalink
feat: Add MsgLeaveGroup to group module (#10887)
Browse files Browse the repository at this point in the history
## Description

Closes: #9657 



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
  • Loading branch information
aleem1314 authored Mar 4, 2022
1 parent 7c0c347 commit 9ef249c
Show file tree
Hide file tree
Showing 18 changed files with 3,185 additions and 262 deletions.
569 changes: 551 additions & 18 deletions api/cosmos/group/v1beta1/events.pulsar.go

Large diffs are not rendered by default.

1,207 changes: 1,073 additions & 134 deletions api/cosmos/group/v1beta1/tx.pulsar.go

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions api/cosmos/group/v1beta1/tx_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions proto/cosmos/group/v1beta1/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ message EventExec {
// proposal_id is the unique ID of the proposal.
uint64 proposal_id = 1;
}

// EventLeaveGroup is an event emitted when group member leaves the group.
message EventLeaveGroup {

// group_id is the unique ID of the group.
uint64 group_id = 1;

// address is the account address of the group member.
string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}
17 changes: 17 additions & 0 deletions proto/cosmos/group/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ service Msg {

// Exec executes a proposal.
rpc Exec(MsgExec) returns (MsgExecResponse);

// LeaveGroup allows a group member to leave the group.
rpc LeaveGroup(MsgLeaveGroup) returns (MsgLeaveGroupResponse);
}

//
Expand Down Expand Up @@ -345,3 +348,17 @@ message MsgExec {

// MsgExecResponse is the Msg/Exec request type.
message MsgExecResponse {}

// MsgLeaveGroup is the Msg/LeaveGroup request type.
message MsgLeaveGroup {
option (cosmos.msg.v1.signer) = "address";

// address is the account address of the group member.
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// group_id is the unique ID of the group.
uint64 group_id = 2;
}

// MsgLeaveGroupResponse is the Msg/LeaveGroup response type.
message MsgLeaveGroupResponse {}
44 changes: 44 additions & 0 deletions x/group/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TxCmd(name string) *cobra.Command {
MsgSubmitProposalCmd(),
MsgVoteCmd(),
MsgExecCmd(),
MsgLeaveGroupCmd(),
)

return txCmd
Expand Down Expand Up @@ -780,3 +781,46 @@ func MsgExecCmd() *cobra.Command {

return cmd
}

// MsgLeaveGroupCmd creates a CLI command for Msg/LeaveGroup.
func MsgLeaveGroupCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "leave-group [member-address] [group-id]",
Short: "remove member from the group",
Long: ` remove member from the group
Parameters:
group-id: unique id of the group
member-address: account address of the group member
Note, the '--from' flag is
ignored as it is implied from [member-address]
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Flags().Set(flags.FlagFrom, args[0])
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.MsgLeaveGroup{
Address: clientCtx.GetFromAddress().String(),
GroupId: 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
}
187 changes: 187 additions & 0 deletions x/group/client/testutil/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,193 @@ func (s *IntegrationTestSuite) TestTxExec() {
}
}

func (s *IntegrationTestSuite) TestTxLeaveGroup() {
val := s.network.Validators[0]
clientCtx := val.ClientCtx
require := s.Require()

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 3 accounts with some tokens
members := make([]string, 3)
for i := 1; i <= 3; i++ {
info, _, err := clientCtx.Keyring.NewMnemonic(fmt.Sprintf("member%d", i), keyring.English, sdk.FullFundraiserPath,
keyring.DefaultBIP39Passphrase, hd.Secp256k1)
require.NoError(err)

pk, err := info.GetPubKey()
require.NoError(err)

account := sdk.AccAddress(pk.Address())
members[i-1] = account.String()

_, err = banktestutil.MsgSendExec(clientCtx, val.Address, account,
sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(100))),
commonFlags...,
)
require.NoError(err)
}

// create a group with three members
validMembers := fmt.Sprintf(`{"members": [{
"address": "%s",
"weight": "1",
"metadata": "AQ=="
},{
"address": "%s",
"weight": "2",
"metadata": "AQ=="
},{
"address": "%s",
"weight": "2",
"metadata": "AQ=="
}]}`, members[0], members[1], members[2])
validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers)
out, err := cli.ExecTestCLICmd(clientCtx, client.MsgCreateGroupCmd(),
append(
[]string{
val.Address.String(),
validMetadata,
validMembersFile.Name(),
},
commonFlags...,
),
)
require.NoError(err, out.String())
var txResp sdk.TxResponse
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String())
groupID := s.getGroupIdFromTxResponse(txResp)

// create group policy
out, err = cli.ExecTestCLICmd(clientCtx, client.MsgCreateGroupPolicyCmd(),
append(
[]string{
val.Address.String(),
groupID,
"AQ==",
"{\"@type\":\"/cosmos.group.v1beta1.ThresholdDecisionPolicy\", \"threshold\":\"3\", \"windows\":{\"voting_period\":\"1s\"}}",
},
commonFlags...,
),
)
require.NoError(err, out.String())

out, err = cli.ExecTestCLICmd(clientCtx, client.QueryGroupPoliciesByGroupCmd(), []string{groupID, fmt.Sprintf("--%s=json", tmcli.OutputFlag)})
require.NoError(err, out.String())
require.NotNil(out)
var resp group.QueryGroupPoliciesByGroupResponse
require.NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp))
require.Len(resp.GroupPolicies, 1)

testCases := []struct {
name string
args []string
expectErr bool
errMsg string
}{
{
"invalid member address",
append(
[]string{
"address",
groupID,
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
},
commonFlags...,
),
true,
"key not found",
},
{
"group not found",
append(
[]string{
members[0],
"40",
fmt.Sprintf("--%s=%s", flags.FlagFrom, members[0]),
},
commonFlags...,
),
true,
"group: not found",
},
{
"valid case",
append(
[]string{
members[2],
groupID,
fmt.Sprintf("--%s=%s", flags.FlagFrom, members[2]),
},
commonFlags...,
),
false,
"",
},
{
"not part of group",
append(
[]string{
members[2],
groupID,
fmt.Sprintf("--%s=%s", flags.FlagFrom, members[2]),
},
commonFlags...,
),
true,
"is not part of group",
},
{
"can leave group policy threshold is more than group weight",
append(
[]string{
members[1],
groupID,
fmt.Sprintf("--%s=%s", flags.FlagFrom, members[1]),
},
commonFlags...,
),
false,
"",
},
}

for _, tc := range testCases {
tc := tc

s.Run(tc.name, func() {
cmd := client.MsgLeaveGroupCmd()
out, err := cli.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
require.Contains(out.String(), tc.errMsg)
} else {
require.NoError(err, out.String())
var resp sdk.TxResponse
require.NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String())
}
})
}
}

func (s *IntegrationTestSuite) getGroupIdFromTxResponse(txResp sdk.TxResponse) string {
s.Require().Greater(len(txResp.Logs), 0)
s.Require().NotNil(txResp.Logs[0].Events)
events := txResp.Logs[0].Events
createProposalEvent, _ := sdk.TypedEventToEvent(&group.EventCreateGroup{})

for _, e := range events {
if e.Type == createProposalEvent.Type {
return strings.ReplaceAll(e.Attributes[0].Value, "\"", "")
}
}

return ""
}

// createCLIProposal writes a CLI proposal with a MsgSend to a file. Returns
// the path to the JSON file.
func (s *IntegrationTestSuite) createCLIProposal(groupPolicyAddress, proposer, sendFrom, sendTo, metadata string) string {
Expand Down
2 changes: 2 additions & 0 deletions x/group/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgWithdrawProposal{}, "cosmos-sdk/group/MsgWithdrawProposal", nil)
cdc.RegisterConcrete(&MsgVote{}, "cosmos-sdk/group/MsgVote", nil)
cdc.RegisterConcrete(&MsgExec{}, "cosmos-sdk/group/MsgExec", nil)
cdc.RegisterConcrete(&MsgLeaveGroup{}, "cosmos-sdk/group/MsgLeaveGroup", nil)
}

func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
Expand All @@ -45,6 +46,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgWithdrawProposal{},
&MsgVote{},
&MsgExec{},
&MsgLeaveGroup{},
)

msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
Expand Down
Loading

0 comments on commit 9ef249c

Please sign in to comment.