cosmos-sdk/x/group/keeper/keeper_test.go

2087 lines
58 KiB
Go
Raw Normal View History

feat: Add server implementation of Group module (#10570) <!-- The default pull request template is for types feat, fix, or refactor. For other templates, add one of the following parameters to the url: - template=docs.md - template=other.md --> ## Description Closes: #9897 Closes: #9905 Adds server implementation of Group module and wires it up in simapp --- ### 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... - [x] 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 - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [x] provided a link to the relevant issue or specification - [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [x] 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` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [x] 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)
2021-12-10 03:02:11 -08:00
package keeper_test
import (
"bytes"
"context"
"sort"
"strings"
"testing"
"time"
gogotypes "github.com/gogo/protobuf/types"
"github.com/stretchr/testify/suite"
tmtime "github.com/tendermint/tendermint/libs/time"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/testutil"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/cosmos/cosmos-sdk/x/group/keeper"
)
type TestSuite struct {
suite.Suite
app *simapp.SimApp
sdkCtx sdk.Context
ctx context.Context
addrs []sdk.AccAddress
groupID uint64
groupAccountAddr sdk.AccAddress
keeper keeper.Keeper
blockTime time.Time
}
func (s *TestSuite) SetupTest() {
app := simapp.Setup(s.T(), false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
s.blockTime = tmtime.Now()
ctx = ctx.WithBlockHeader(tmproto.Header{Time: s.blockTime})
s.app = app
s.sdkCtx = ctx
s.ctx = sdk.WrapSDKContext(ctx)
s.keeper = s.app.GroupKeeper
s.addrs = simapp.AddTestAddrsIncremental(app, ctx, 6, sdk.NewInt(30000000))
// Initial group, group account and balance setup
members := []group.Member{
{Address: s.addrs[4].String(), Weight: "1"}, {Address: s.addrs[1].String(), Weight: "2"},
}
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: s.addrs[0].String(),
Members: members,
Metadata: nil,
})
s.Require().NoError(err)
s.groupID = groupRes.GroupId
policy := group.NewThresholdDecisionPolicy(
"2",
time.Second,
)
accountReq := &group.MsgCreateGroupAccount{
Admin: s.addrs[0].String(),
GroupId: s.groupID,
Metadata: nil,
}
err = accountReq.SetDecisionPolicy(policy)
s.Require().NoError(err)
accountRes, err := s.keeper.CreateGroupAccount(s.ctx, accountReq)
s.Require().NoError(err)
addr, err := sdk.AccAddressFromBech32(accountRes.Address)
s.Require().NoError(err)
s.groupAccountAddr = addr
s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, s.sdkCtx, s.groupAccountAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(TestSuite))
}
func (s *TestSuite) TestCreateGroup() {
addrs := s.addrs
addr1 := addrs[0]
addr3 := addrs[2]
addr5 := addrs[4]
addr6 := addrs[5]
members := []group.Member{{
Address: addr5.String(),
Weight: "1",
Metadata: nil,
}, {
Address: addr6.String(),
Weight: "2",
Metadata: nil,
}}
expGroups := []*group.GroupInfo{
{
GroupId: s.groupID,
Version: 1,
Admin: addr1.String(),
TotalWeight: "3",
Metadata: nil,
},
{
GroupId: 2,
Version: 1,
Admin: addr1.String(),
TotalWeight: "3",
Metadata: nil,
},
}
specs := map[string]struct {
req *group.MsgCreateGroup
expErr bool
expGroups []*group.GroupInfo
}{
"all good": {
req: &group.MsgCreateGroup{
Admin: addr1.String(),
Members: members,
Metadata: nil,
},
expGroups: expGroups,
},
"group metadata too long": {
req: &group.MsgCreateGroup{
Admin: addr1.String(),
Members: members,
Metadata: bytes.Repeat([]byte{1}, 256),
},
expErr: true,
},
"member metadata too long": {
req: &group.MsgCreateGroup{
Admin: addr1.String(),
Members: []group.Member{{
Address: addr3.String(),
Weight: "1",
Metadata: bytes.Repeat([]byte{1}, 256),
}},
Metadata: nil,
},
expErr: true,
},
"zero member weight": {
req: &group.MsgCreateGroup{
Admin: addr1.String(),
Members: []group.Member{{
Address: addr3.String(),
Weight: "0",
Metadata: nil,
}},
Metadata: nil,
},
expErr: true,
},
}
var seq uint32 = 1
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
res, err := s.keeper.CreateGroup(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
_, err := s.keeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: uint64(seq + 1)})
s.Require().Error(err)
return
}
s.Require().NoError(err)
id := res.GroupId
seq++
s.Assert().Equal(uint64(seq), id)
// then all data persisted
loadedGroupRes, err := s.keeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id})
s.Require().NoError(err)
s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin)
s.Assert().Equal(spec.req.Metadata, loadedGroupRes.Info.Metadata)
s.Assert().Equal(id, loadedGroupRes.Info.GroupId)
s.Assert().Equal(uint64(1), loadedGroupRes.Info.Version)
// and members are stored as well
membersRes, err := s.keeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id})
s.Require().NoError(err)
loadedMembers := membersRes.Members
s.Require().Equal(len(members), len(loadedMembers))
// we reorder members by address to be able to compare them
sort.Slice(members, func(i, j int) bool {
addri, err := sdk.AccAddressFromBech32(members[i].Address)
s.Require().NoError(err)
addrj, err := sdk.AccAddressFromBech32(members[j].Address)
s.Require().NoError(err)
return bytes.Compare(addri, addrj) < 0
})
for i := range loadedMembers {
s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata)
s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address)
s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight)
s.Assert().Equal(id, loadedMembers[i].GroupId)
}
// query groups by admin
groupsRes, err := s.keeper.GroupsByAdmin(s.ctx, &group.QueryGroupsByAdminRequest{Admin: addr1.String()})
s.Require().NoError(err)
loadedGroups := groupsRes.Groups
s.Require().Equal(len(spec.expGroups), len(loadedGroups))
for i := range loadedGroups {
s.Assert().Equal(spec.expGroups[i].Metadata, loadedGroups[i].Metadata)
s.Assert().Equal(spec.expGroups[i].Admin, loadedGroups[i].Admin)
s.Assert().Equal(spec.expGroups[i].TotalWeight, loadedGroups[i].TotalWeight)
s.Assert().Equal(spec.expGroups[i].GroupId, loadedGroups[i].GroupId)
s.Assert().Equal(spec.expGroups[i].Version, loadedGroups[i].Version)
}
})
}
}
func (s *TestSuite) TestUpdateGroupAdmin() {
addrs := s.addrs
addr1 := addrs[0]
addr2 := addrs[1]
addr3 := addrs[2]
addr4 := addrs[3]
members := []group.Member{{
Address: addr1.String(),
Weight: "1",
Metadata: nil,
}}
oldAdmin := addr2.String()
newAdmin := addr3.String()
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: oldAdmin,
Members: members,
Metadata: nil,
})
s.Require().NoError(err)
groupID := groupRes.GroupId
specs := map[string]struct {
req *group.MsgUpdateGroupAdmin
expStored *group.GroupInfo
expErr bool
}{
"with correct admin": {
req: &group.MsgUpdateGroupAdmin{
GroupId: groupID,
Admin: oldAdmin,
NewAdmin: newAdmin,
},
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: newAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 2,
},
},
"with wrong admin": {
req: &group.MsgUpdateGroupAdmin{
GroupId: groupID,
Admin: addr4.String(),
NewAdmin: newAdmin,
},
expErr: true,
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: oldAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
},
"with unknown groupID": {
req: &group.MsgUpdateGroupAdmin{
GroupId: 999,
Admin: oldAdmin,
NewAdmin: newAdmin,
},
expErr: true,
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: oldAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
_, err := s.keeper.UpdateGroupAdmin(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
// then
res, err := s.keeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: groupID})
s.Require().NoError(err)
s.Assert().Equal(spec.expStored, res.Info)
})
}
}
func (s *TestSuite) TestUpdateGroupMetadata() {
addrs := s.addrs
addr1 := addrs[0]
addr3 := addrs[2]
oldAdmin := addr1.String()
groupID := s.groupID
specs := map[string]struct {
req *group.MsgUpdateGroupMetadata
expErr bool
expStored *group.GroupInfo
}{
"with correct admin": {
req: &group.MsgUpdateGroupMetadata{
GroupId: groupID,
Admin: oldAdmin,
Metadata: []byte{1, 2, 3},
},
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: oldAdmin,
Metadata: []byte{1, 2, 3},
TotalWeight: "3",
Version: 2,
},
},
"with wrong admin": {
req: &group.MsgUpdateGroupMetadata{
GroupId: groupID,
Admin: addr3.String(),
Metadata: []byte{1, 2, 3},
},
expErr: true,
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: oldAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
},
"with unknown groupid": {
req: &group.MsgUpdateGroupMetadata{
GroupId: 999,
Admin: oldAdmin,
Metadata: []byte{1, 2, 3},
},
expErr: true,
expStored: &group.GroupInfo{
GroupId: groupID,
Admin: oldAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
sdkCtx, _ := s.sdkCtx.CacheContext()
ctx := sdk.WrapSDKContext(sdkCtx)
_, err := s.keeper.UpdateGroupMetadata(ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
// then
res, err := s.keeper.GroupInfo(ctx, &group.QueryGroupInfoRequest{GroupId: groupID})
s.Require().NoError(err)
s.Assert().Equal(spec.expStored, res.Info)
})
}
}
func (s *TestSuite) TestUpdateGroupMembers() {
addrs := s.addrs
addr3 := addrs[2]
addr4 := addrs[3]
addr5 := addrs[4]
addr6 := addrs[5]
member1 := addr5.String()
member2 := addr6.String()
members := []group.Member{{
Address: member1,
Weight: "1",
Metadata: nil,
}}
myAdmin := addr4.String()
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: myAdmin,
Members: members,
Metadata: nil,
})
s.Require().NoError(err)
groupID := groupRes.GroupId
specs := map[string]struct {
req *group.MsgUpdateGroupMembers
expErr bool
expGroup *group.GroupInfo
expMembers []*group.GroupMember
}{
"add new member": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: member2,
Weight: "2",
Metadata: nil,
}},
},
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "3",
Version: 2,
},
expMembers: []*group.GroupMember{
{
Member: &group.Member{
Address: member2,
Weight: "2",
Metadata: nil,
},
GroupId: groupID,
},
{
Member: &group.Member{
Address: member1,
Weight: "1",
Metadata: nil,
},
GroupId: groupID,
},
},
},
"update member": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: member1,
Weight: "2",
Metadata: []byte{1, 2, 3},
}},
},
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "2",
Version: 2,
},
expMembers: []*group.GroupMember{
{
GroupId: groupID,
Member: &group.Member{
Address: member1,
Weight: "2",
Metadata: []byte{1, 2, 3},
},
},
},
},
"update member with same data": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: member1,
Weight: "1",
}},
},
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 2,
},
expMembers: []*group.GroupMember{
{
GroupId: groupID,
Member: &group.Member{
Address: member1,
Weight: "1",
},
},
},
},
"replace member": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{
{
Address: member1,
Weight: "0",
Metadata: nil,
},
{
Address: member2,
Weight: "1",
Metadata: nil,
},
},
},
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 2,
},
expMembers: []*group.GroupMember{{
GroupId: groupID,
Member: &group.Member{
Address: member2,
Weight: "1",
Metadata: nil,
},
}},
},
"remove existing member": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: member1,
Weight: "0",
Metadata: nil,
}},
},
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "0",
Version: 2,
},
expMembers: []*group.GroupMember{},
},
"remove unknown member": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: addr4.String(),
Weight: "0",
Metadata: nil,
}},
},
expErr: true,
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
expMembers: []*group.GroupMember{{
GroupId: groupID,
Member: &group.Member{
Address: member1,
Weight: "1",
Metadata: nil,
},
}},
},
"with wrong admin": {
req: &group.MsgUpdateGroupMembers{
GroupId: groupID,
Admin: addr3.String(),
MemberUpdates: []group.Member{{
Address: member1,
Weight: "2",
Metadata: nil,
}},
},
expErr: true,
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
expMembers: []*group.GroupMember{{
GroupId: groupID,
Member: &group.Member{
Address: member1,
Weight: "1",
},
}},
},
"with unknown groupID": {
req: &group.MsgUpdateGroupMembers{
GroupId: 999,
Admin: myAdmin,
MemberUpdates: []group.Member{{
Address: member1,
Weight: "2",
Metadata: nil,
}},
},
expErr: true,
expGroup: &group.GroupInfo{
GroupId: groupID,
Admin: myAdmin,
Metadata: nil,
TotalWeight: "1",
Version: 1,
},
expMembers: []*group.GroupMember{{
GroupId: groupID,
Member: &group.Member{
Address: member1,
Weight: "1",
},
}},
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
sdkCtx, _ := s.sdkCtx.CacheContext()
ctx := sdk.WrapSDKContext(sdkCtx)
_, err := s.keeper.UpdateGroupMembers(ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
// then
res, err := s.keeper.GroupInfo(ctx, &group.QueryGroupInfoRequest{GroupId: groupID})
s.Require().NoError(err)
s.Assert().Equal(spec.expGroup, res.Info)
// and members persisted
membersRes, err := s.keeper.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupID})
s.Require().NoError(err)
loadedMembers := membersRes.Members
s.Require().Equal(len(spec.expMembers), len(loadedMembers))
// we reorder group members by address to be able to compare them
sort.Slice(spec.expMembers, func(i, j int) bool {
addri, err := sdk.AccAddressFromBech32(spec.expMembers[i].Member.Address)
s.Require().NoError(err)
addrj, err := sdk.AccAddressFromBech32(spec.expMembers[j].Member.Address)
s.Require().NoError(err)
return bytes.Compare(addri, addrj) < 0
})
for i := range loadedMembers {
s.Assert().Equal(spec.expMembers[i].Member.Metadata, loadedMembers[i].Member.Metadata)
s.Assert().Equal(spec.expMembers[i].Member.Address, loadedMembers[i].Member.Address)
s.Assert().Equal(spec.expMembers[i].Member.Weight, loadedMembers[i].Member.Weight)
s.Assert().Equal(spec.expMembers[i].GroupId, loadedMembers[i].GroupId)
}
})
}
}
func (s *TestSuite) TestCreateGroupAccount() {
addrs := s.addrs
addr1 := addrs[0]
addr4 := addrs[3]
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: addr1.String(),
Members: nil,
Metadata: nil,
})
s.Require().NoError(err)
myGroupID := groupRes.GroupId
specs := map[string]struct {
req *group.MsgCreateGroupAccount
policy group.DecisionPolicy
expErr bool
}{
"all good": {
req: &group.MsgCreateGroupAccount{
Admin: addr1.String(),
Metadata: nil,
GroupId: myGroupID,
},
policy: group.NewThresholdDecisionPolicy(
"1",
time.Second,
),
},
"decision policy threshold > total group weight": {
req: &group.MsgCreateGroupAccount{
Admin: addr1.String(),
Metadata: nil,
GroupId: myGroupID,
},
policy: group.NewThresholdDecisionPolicy(
"10",
time.Second,
),
},
"group id does not exists": {
req: &group.MsgCreateGroupAccount{
Admin: addr1.String(),
Metadata: nil,
GroupId: 9999,
},
policy: group.NewThresholdDecisionPolicy(
"1",
time.Second,
),
expErr: true,
},
"admin not group admin": {
req: &group.MsgCreateGroupAccount{
Admin: addr4.String(),
Metadata: nil,
GroupId: myGroupID,
},
policy: group.NewThresholdDecisionPolicy(
"1",
time.Second,
),
expErr: true,
},
"metadata too long": {
req: &group.MsgCreateGroupAccount{
Admin: addr1.String(),
Metadata: []byte(strings.Repeat("a", 256)),
GroupId: myGroupID,
},
policy: group.NewThresholdDecisionPolicy(
"1",
time.Second,
),
expErr: true,
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
err := spec.req.SetDecisionPolicy(spec.policy)
s.Require().NoError(err)
res, err := s.keeper.CreateGroupAccount(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
addr := res.Address
// then all data persisted
groupAccountRes, err := s.keeper.GroupAccountInfo(s.ctx, &group.QueryGroupAccountInfoRequest{Address: addr})
s.Require().NoError(err)
groupAccount := groupAccountRes.Info
s.Assert().Equal(addr, groupAccount.Address)
s.Assert().Equal(myGroupID, groupAccount.GroupId)
s.Assert().Equal(spec.req.Admin, groupAccount.Admin)
s.Assert().Equal(spec.req.Metadata, groupAccount.Metadata)
s.Assert().Equal(uint64(1), groupAccount.Version)
s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), groupAccount.GetDecisionPolicy())
})
}
}
func (s *TestSuite) TestUpdateGroupAccountAdmin() {
addrs := s.addrs
addr1 := addrs[0]
addr2 := addrs[1]
addr5 := addrs[4]
admin, newAdmin := addr1, addr2
groupAccountAddr, myGroupID, policy := createGroupAndGroupAccount(admin, s)
specs := map[string]struct {
req *group.MsgUpdateGroupAccountAdmin
expGroupAccount *group.GroupAccountInfo
expErr bool
}{
"with wrong admin": {
req: &group.MsgUpdateGroupAccountAdmin{
Admin: addr5.String(),
Address: groupAccountAddr,
NewAdmin: newAdmin.String(),
},
expGroupAccount: &group.GroupAccountInfo{
Admin: admin.String(),
Address: groupAccountAddr,
GroupId: myGroupID,
Metadata: nil,
Version: 2,
DecisionPolicy: nil,
},
expErr: true,
},
"with wrong group account": {
req: &group.MsgUpdateGroupAccountAdmin{
Admin: admin.String(),
Address: addr5.String(),
NewAdmin: newAdmin.String(),
},
expGroupAccount: &group.GroupAccountInfo{
Admin: admin.String(),
Address: groupAccountAddr,
GroupId: myGroupID,
Metadata: nil,
Version: 2,
DecisionPolicy: nil,
},
expErr: true,
},
"correct data": {
req: &group.MsgUpdateGroupAccountAdmin{
Admin: admin.String(),
Address: groupAccountAddr,
NewAdmin: newAdmin.String(),
},
expGroupAccount: &group.GroupAccountInfo{
Admin: newAdmin.String(),
Address: groupAccountAddr,
GroupId: myGroupID,
Metadata: nil,
Version: 2,
DecisionPolicy: nil,
},
expErr: false,
},
}
for msg, spec := range specs {
spec := spec
err := spec.expGroupAccount.SetDecisionPolicy(policy)
s.Require().NoError(err)
s.Run(msg, func() {
_, err := s.keeper.UpdateGroupAccountAdmin(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
res, err := s.keeper.GroupAccountInfo(s.ctx, &group.QueryGroupAccountInfoRequest{
Address: groupAccountAddr,
})
s.Require().NoError(err)
s.Assert().Equal(spec.expGroupAccount, res.Info)
})
}
}
func (s *TestSuite) TestUpdateGroupAccountMetadata() {
addrs := s.addrs
addr1 := addrs[0]
addr5 := addrs[4]
admin := addr1
groupAccountAddr, myGroupID, policy := createGroupAndGroupAccount(admin, s)
specs := map[string]struct {
req *group.MsgUpdateGroupAccountMetadata
expGroupAccount *group.GroupAccountInfo
expErr bool
}{
"with wrong admin": {
req: &group.MsgUpdateGroupAccountMetadata{
Admin: addr5.String(),
Address: groupAccountAddr,
Metadata: []byte("hello"),
},
expGroupAccount: &group.GroupAccountInfo{},
expErr: true,
},
"with wrong group account": {
req: &group.MsgUpdateGroupAccountMetadata{
Admin: admin.String(),
Address: addr5.String(),
Metadata: []byte("hello"),
},
expGroupAccount: &group.GroupAccountInfo{},
expErr: true,
},
"with comment too long": {
req: &group.MsgUpdateGroupAccountMetadata{
Admin: admin.String(),
Address: addr5.String(),
Metadata: []byte(strings.Repeat("a", 256)),
},
expGroupAccount: &group.GroupAccountInfo{},
expErr: true,
},
"correct data": {
req: &group.MsgUpdateGroupAccountMetadata{
Admin: admin.String(),
Address: groupAccountAddr,
Metadata: []byte("hello"),
},
expGroupAccount: &group.GroupAccountInfo{
Admin: admin.String(),
Address: groupAccountAddr,
GroupId: myGroupID,
Metadata: []byte("hello"),
Version: 2,
DecisionPolicy: nil,
},
expErr: false,
},
}
for msg, spec := range specs {
spec := spec
err := spec.expGroupAccount.SetDecisionPolicy(policy)
s.Require().NoError(err)
s.Run(msg, func() {
_, err := s.keeper.UpdateGroupAccountMetadata(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
res, err := s.keeper.GroupAccountInfo(s.ctx, &group.QueryGroupAccountInfoRequest{
Address: groupAccountAddr,
})
s.Require().NoError(err)
s.Assert().Equal(spec.expGroupAccount, res.Info)
})
}
}
func (s *TestSuite) TestUpdateGroupAccountDecisionPolicy() {
addrs := s.addrs
addr1 := addrs[0]
addr5 := addrs[4]
admin := addr1
groupAccountAddr, myGroupID, policy := createGroupAndGroupAccount(admin, s)
specs := map[string]struct {
req *group.MsgUpdateGroupAccountDecisionPolicy
policy group.DecisionPolicy
expGroupAccount *group.GroupAccountInfo
expErr bool
}{
"with wrong admin": {
req: &group.MsgUpdateGroupAccountDecisionPolicy{
Admin: addr5.String(),
Address: groupAccountAddr,
},
policy: policy,
expGroupAccount: &group.GroupAccountInfo{},
expErr: true,
},
"with wrong group account": {
req: &group.MsgUpdateGroupAccountDecisionPolicy{
Admin: admin.String(),
Address: addr5.String(),
},
policy: policy,
expGroupAccount: &group.GroupAccountInfo{},
expErr: true,
},
"correct data": {
req: &group.MsgUpdateGroupAccountDecisionPolicy{
Admin: admin.String(),
Address: groupAccountAddr,
},
policy: group.NewThresholdDecisionPolicy(
"2",
time.Duration(2)*time.Second,
),
expGroupAccount: &group.GroupAccountInfo{
Admin: admin.String(),
Address: groupAccountAddr,
GroupId: myGroupID,
Metadata: nil,
Version: 2,
DecisionPolicy: nil,
},
expErr: false,
},
}
for msg, spec := range specs {
spec := spec
err := spec.expGroupAccount.SetDecisionPolicy(spec.policy)
s.Require().NoError(err)
err = spec.req.SetDecisionPolicy(spec.policy)
s.Require().NoError(err)
s.Run(msg, func() {
_, err := s.keeper.UpdateGroupAccountDecisionPolicy(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
res, err := s.keeper.GroupAccountInfo(s.ctx, &group.QueryGroupAccountInfoRequest{
Address: groupAccountAddr,
})
s.Require().NoError(err)
s.Assert().Equal(spec.expGroupAccount, res.Info)
})
}
}
func (s *TestSuite) TestGroupAccountsByAdminOrGroup() {
addrs := s.addrs
addr2 := addrs[1]
admin := addr2
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: admin.String(),
Members: nil,
Metadata: nil,
})
s.Require().NoError(err)
myGroupID := groupRes.GroupId
policies := []group.DecisionPolicy{
group.NewThresholdDecisionPolicy(
"1",
time.Second,
),
group.NewThresholdDecisionPolicy(
"10",
time.Second,
),
}
count := 2
expectAccs := make([]*group.GroupAccountInfo, count)
for i := range expectAccs {
req := &group.MsgCreateGroupAccount{
Admin: admin.String(),
Metadata: nil,
GroupId: myGroupID,
}
err := req.SetDecisionPolicy(policies[i])
s.Require().NoError(err)
res, err := s.keeper.CreateGroupAccount(s.ctx, req)
s.Require().NoError(err)
expectAcc := &group.GroupAccountInfo{
Address: res.Address,
Admin: admin.String(),
Metadata: nil,
GroupId: myGroupID,
Version: uint64(1),
}
err = expectAcc.SetDecisionPolicy(policies[i])
s.Require().NoError(err)
expectAccs[i] = expectAcc
}
sort.Slice(expectAccs, func(i, j int) bool { return expectAccs[i].Address < expectAccs[j].Address })
// query group account by group
accountsByGroupRes, err := s.keeper.GroupAccountsByGroup(s.ctx, &group.QueryGroupAccountsByGroupRequest{
GroupId: myGroupID,
})
s.Require().NoError(err)
accounts := accountsByGroupRes.GroupAccounts
s.Require().Equal(len(accounts), count)
// we reorder accounts by address to be able to compare them
sort.Slice(accounts, func(i, j int) bool { return accounts[i].Address < accounts[j].Address })
for i := range accounts {
s.Assert().Equal(accounts[i].Address, expectAccs[i].Address)
s.Assert().Equal(accounts[i].GroupId, expectAccs[i].GroupId)
s.Assert().Equal(accounts[i].Admin, expectAccs[i].Admin)
s.Assert().Equal(accounts[i].Metadata, expectAccs[i].Metadata)
s.Assert().Equal(accounts[i].Version, expectAccs[i].Version)
s.Assert().Equal(accounts[i].GetDecisionPolicy(), expectAccs[i].GetDecisionPolicy())
}
// query group account by admin
accountsByAdminRes, err := s.keeper.GroupAccountsByAdmin(s.ctx, &group.QueryGroupAccountsByAdminRequest{
Admin: admin.String(),
})
s.Require().NoError(err)
accounts = accountsByAdminRes.GroupAccounts
s.Require().Equal(len(accounts), count)
// we reorder accounts by address to be able to compare them
sort.Slice(accounts, func(i, j int) bool { return accounts[i].Address < accounts[j].Address })
for i := range accounts {
s.Assert().Equal(accounts[i].Address, expectAccs[i].Address)
s.Assert().Equal(accounts[i].GroupId, expectAccs[i].GroupId)
s.Assert().Equal(accounts[i].Admin, expectAccs[i].Admin)
s.Assert().Equal(accounts[i].Metadata, expectAccs[i].Metadata)
s.Assert().Equal(accounts[i].Version, expectAccs[i].Version)
s.Assert().Equal(accounts[i].GetDecisionPolicy(), expectAccs[i].GetDecisionPolicy())
}
}
func (s *TestSuite) TestCreateProposal() {
addrs := s.addrs
addr1 := addrs[0]
addr2 := addrs[1]
addr4 := addrs[3]
addr5 := addrs[4]
myGroupID := s.groupID
accountAddr := s.groupAccountAddr
msgSend := &banktypes.MsgSend{
FromAddress: s.groupAccountAddr.String(),
ToAddress: addr2.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)},
}
accountReq := &group.MsgCreateGroupAccount{
Admin: addr1.String(),
GroupId: myGroupID,
Metadata: nil,
}
policy := group.NewThresholdDecisionPolicy(
"100",
time.Second,
)
err := accountReq.SetDecisionPolicy(policy)
s.Require().NoError(err)
bigThresholdRes, err := s.keeper.CreateGroupAccount(s.ctx, accountReq)
s.Require().NoError(err)
bigThresholdAddr := bigThresholdRes.Address
defaultProposal := group.Proposal{
Status: group.ProposalStatusSubmitted,
Result: group.ProposalResultUnfinalized,
VoteState: group.Tally{
YesCount: "0",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
ExecutorResult: group.ProposalExecutorResultNotRun,
}
specs := map[string]struct {
req *group.MsgCreateProposal
msgs []sdk.Msg
expProposal group.Proposal
expErr bool
postRun func(sdkCtx sdk.Context)
}{
"all good with minimal fields set": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr2.String()},
},
expProposal: defaultProposal,
postRun: func(sdkCtx sdk.Context) {},
},
"all good with good msg payload": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr2.String()},
},
msgs: []sdk.Msg{&banktypes.MsgSend{
FromAddress: accountAddr.String(),
ToAddress: addr2.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("token", 100)},
}},
expProposal: defaultProposal,
postRun: func(sdkCtx sdk.Context) {},
},
"metadata too long": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Metadata: bytes.Repeat([]byte{1}, 256),
Proposers: []string{addr2.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"group account required": {
req: &group.MsgCreateProposal{
Metadata: nil,
Proposers: []string{addr2.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"existing group account required": {
req: &group.MsgCreateProposal{
Address: addr1.String(),
Proposers: []string{addr2.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"impossible case: decision policy threshold > total group weight": {
req: &group.MsgCreateProposal{
Address: bigThresholdAddr,
Proposers: []string{addr2.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"only group members can create a proposal": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr4.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"all proposers must be in group": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr2.String(), addr4.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"admin that is not a group member can not create proposal": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Metadata: nil,
Proposers: []string{addr1.String()},
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"reject msgs that are not authz by group account": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Metadata: nil,
Proposers: []string{addr2.String()},
},
msgs: []sdk.Msg{&testdata.TestMsg{Signers: []string{addr1.String()}}},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"with try exec": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr2.String()},
Exec: group.Exec_EXEC_TRY,
},
msgs: []sdk.Msg{msgSend},
expProposal: group.Proposal{
Status: group.ProposalStatusClosed,
Result: group.ProposalResultAccepted,
VoteState: group.Tally{
YesCount: "2",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
ExecutorResult: group.ProposalExecutorResultSuccess,
},
postRun: func(sdkCtx sdk.Context) {
fromBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, accountAddr)
s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900))
toBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, addr2)
s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100))
},
},
"with try exec, not enough yes votes for proposal to pass": {
req: &group.MsgCreateProposal{
Address: accountAddr.String(),
Proposers: []string{addr5.String()},
Exec: group.Exec_EXEC_TRY,
},
msgs: []sdk.Msg{msgSend},
expProposal: group.Proposal{
Status: group.ProposalStatusSubmitted,
Result: group.ProposalResultUnfinalized,
VoteState: group.Tally{
YesCount: "1",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
ExecutorResult: group.ProposalExecutorResultNotRun,
},
postRun: func(sdkCtx sdk.Context) {},
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
err := spec.req.SetMsgs(spec.msgs)
s.Require().NoError(err)
res, err := s.keeper.CreateProposal(s.ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
id := res.ProposalId
// then all data persisted
proposalRes, err := s.keeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: id})
s.Require().NoError(err)
proposal := proposalRes.Proposal
s.Assert().Equal(accountAddr.String(), proposal.Address)
s.Assert().Equal(spec.req.Metadata, proposal.Metadata)
s.Assert().Equal(spec.req.Proposers, proposal.Proposers)
s.Assert().Equal(s.blockTime, proposal.SubmittedAt)
s.Assert().Equal(uint64(1), proposal.GroupVersion)
s.Assert().Equal(uint64(1), proposal.GroupAccountVersion)
s.Assert().Equal(spec.expProposal.Status, proposal.Status)
s.Assert().Equal(spec.expProposal.Result, proposal.Result)
s.Assert().Equal(spec.expProposal.VoteState, proposal.VoteState)
s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult)
s.Assert().Equal(s.blockTime.Add(time.Second), proposal.Timeout)
if spec.msgs == nil { // then empty list is ok
s.Assert().Len(proposal.GetMsgs(), 0)
} else {
s.Assert().Equal(spec.msgs, proposal.GetMsgs())
}
spec.postRun(s.sdkCtx)
})
}
}
func (s *TestSuite) TestVote() {
addrs := s.addrs
addr1 := addrs[0]
addr2 := addrs[1]
addr3 := addrs[2]
addr4 := addrs[3]
addr5 := addrs[4]
members := []group.Member{
{Address: addr4.String(), Weight: "1"},
{Address: addr3.String(), Weight: "2"},
}
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: addr1.String(),
Members: members,
Metadata: nil,
})
s.Require().NoError(err)
myGroupID := groupRes.GroupId
policy := group.NewThresholdDecisionPolicy(
"2",
time.Duration(2),
)
accountReq := &group.MsgCreateGroupAccount{
Admin: addr1.String(),
GroupId: myGroupID,
Metadata: nil,
}
err = accountReq.SetDecisionPolicy(policy)
s.Require().NoError(err)
accountRes, err := s.keeper.CreateGroupAccount(s.ctx, accountReq)
s.Require().NoError(err)
accountAddr := accountRes.Address
groupAccount, err := sdk.AccAddressFromBech32(accountAddr)
s.Require().NoError(err)
s.Require().NotNil(groupAccount)
s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, s.sdkCtx, groupAccount, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
req := &group.MsgCreateProposal{
Address: accountAddr,
Metadata: nil,
Proposers: []string{addr4.String()},
Msgs: nil,
}
err = req.SetMsgs([]sdk.Msg{&banktypes.MsgSend{
FromAddress: accountAddr,
ToAddress: addr5.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)},
}})
s.Require().NoError(err)
proposalRes, err := s.keeper.CreateProposal(s.ctx, req)
s.Require().NoError(err)
myProposalID := proposalRes.ProposalId
// proposals by group account
proposalsRes, err := s.keeper.ProposalsByGroupAccount(s.ctx, &group.QueryProposalsByGroupAccountRequest{
Address: accountAddr,
})
s.Require().NoError(err)
proposals := proposalsRes.Proposals
s.Require().Equal(len(proposals), 1)
s.Assert().Equal(req.Address, proposals[0].Address)
s.Assert().Equal(req.Metadata, proposals[0].Metadata)
s.Assert().Equal(req.Proposers, proposals[0].Proposers)
psubmittedAt, err := gogotypes.TimestampProto(proposals[0].SubmittedAt)
s.Require().NoError(err)
submittedAt, err := gogotypes.TimestampFromProto(psubmittedAt)
s.Require().NoError(err)
s.Assert().Equal(s.blockTime, submittedAt)
s.Assert().Equal(uint64(1), proposals[0].GroupVersion)
s.Assert().Equal(uint64(1), proposals[0].GroupAccountVersion)
s.Assert().Equal(group.ProposalStatusSubmitted, proposals[0].Status)
s.Assert().Equal(group.ProposalResultUnfinalized, proposals[0].Result)
s.Assert().Equal(group.Tally{
YesCount: "0",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
}, proposals[0].VoteState)
specs := map[string]struct {
srcCtx sdk.Context
expVoteState group.Tally
req *group.MsgVote
doBefore func(ctx context.Context)
postRun func(sdkCtx sdk.Context)
expProposalStatus group.Proposal_Status
expResult group.Proposal_Result
expExecutorResult group.Proposal_ExecutorResult
expErr bool
}{
"vote yes": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_YES,
},
expVoteState: group.Tally{
YesCount: "1",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusSubmitted,
expResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"with try exec": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr3.String(),
Choice: group.Choice_CHOICE_YES,
Exec: group.Exec_EXEC_TRY,
},
expVoteState: group.Tally{
YesCount: "2",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusClosed,
expResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultSuccess,
postRun: func(sdkCtx sdk.Context) {
fromBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, groupAccount)
s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900))
toBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, addr5)
s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100))
},
},
"with try exec, not enough yes votes for proposal to pass": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_YES,
Exec: group.Exec_EXEC_TRY,
},
expVoteState: group.Tally{
YesCount: "1",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusSubmitted,
expResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"vote no": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
expVoteState: group.Tally{
YesCount: "0",
NoCount: "1",
AbstainCount: "0",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusSubmitted,
expResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"vote abstain": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_ABSTAIN,
},
expVoteState: group.Tally{
YesCount: "0",
NoCount: "0",
AbstainCount: "1",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusSubmitted,
expResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"vote veto": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_VETO,
},
expVoteState: group.Tally{
YesCount: "0",
NoCount: "0",
AbstainCount: "0",
VetoCount: "1",
},
expProposalStatus: group.ProposalStatusSubmitted,
expResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"apply decision policy early": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr3.String(),
Choice: group.Choice_CHOICE_YES,
},
expVoteState: group.Tally{
YesCount: "2",
NoCount: "0",
AbstainCount: "0",
VetoCount: "0",
},
expProposalStatus: group.ProposalStatusClosed,
expResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultNotRun,
postRun: func(sdkCtx sdk.Context) {},
},
"reject new votes when final decision is made already": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_YES,
},
doBefore: func(ctx context.Context) {
_, err := s.keeper.Vote(ctx, &group.MsgVote{
ProposalId: myProposalID,
Voter: addr3.String(),
Choice: group.Choice_CHOICE_VETO,
})
s.Require().NoError(err)
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"metadata too long": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Metadata: bytes.Repeat([]byte{1}, 256),
Choice: group.Choice_CHOICE_NO,
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"existing proposal required": {
req: &group.MsgVote{
ProposalId: 999,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"empty choice": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"invalid choice": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: 5,
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"voter must be in group": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr2.String(),
Choice: group.Choice_CHOICE_NO,
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"admin that is not a group member can not vote": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr1.String(),
Choice: group.Choice_CHOICE_NO,
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"on timeout": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
srcCtx: s.sdkCtx.WithBlockTime(s.blockTime.Add(time.Second)),
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"closed already": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
doBefore: func(ctx context.Context) {
_, err := s.keeper.Vote(ctx, &group.MsgVote{
ProposalId: myProposalID,
Voter: addr3.String(),
Choice: group.Choice_CHOICE_YES,
})
s.Require().NoError(err)
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"voted already": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
doBefore: func(ctx context.Context) {
_, err := s.keeper.Vote(ctx, &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_YES,
})
s.Require().NoError(err)
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"with group modified": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
doBefore: func(ctx context.Context) {
_, err = s.keeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{
GroupId: myGroupID,
Admin: addr1.String(),
Metadata: []byte{1, 2, 3},
})
s.Require().NoError(err)
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"with policy modified": {
req: &group.MsgVote{
ProposalId: myProposalID,
Voter: addr4.String(),
Choice: group.Choice_CHOICE_NO,
},
doBefore: func(ctx context.Context) {
m, err := group.NewMsgUpdateGroupAccountDecisionPolicyRequest(
addr1,
groupAccount,
&group.ThresholdDecisionPolicy{
Threshold: "1",
Timeout: time.Second,
},
)
s.Require().NoError(err)
_, err = s.keeper.UpdateGroupAccountDecisionPolicy(ctx, m)
s.Require().NoError(err)
},
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
sdkCtx := s.sdkCtx
if !spec.srcCtx.IsZero() {
sdkCtx = spec.srcCtx
}
sdkCtx, _ = sdkCtx.CacheContext()
ctx := sdk.WrapSDKContext(sdkCtx)
if spec.doBefore != nil {
spec.doBefore(ctx)
}
_, err := s.keeper.Vote(ctx, spec.req)
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
s.Require().NoError(err)
// vote is stored and all data persisted
res, err := s.keeper.VoteByProposalVoter(ctx, &group.QueryVoteByProposalVoterRequest{
ProposalId: spec.req.ProposalId,
Voter: spec.req.Voter,
})
s.Require().NoError(err)
loaded := res.Vote
s.Assert().Equal(spec.req.ProposalId, loaded.ProposalId)
s.Assert().Equal(spec.req.Voter, loaded.Voter)
s.Assert().Equal(spec.req.Choice, loaded.Choice)
s.Assert().Equal(spec.req.Metadata, loaded.Metadata)
lsubmittedAt, err := gogotypes.TimestampProto(loaded.SubmittedAt)
s.Require().NoError(err)
submittedAt, err := gogotypes.TimestampFromProto(lsubmittedAt)
s.Require().NoError(err)
s.Assert().Equal(s.blockTime, submittedAt)
// query votes by proposal
votesByProposalRes, err := s.keeper.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{
ProposalId: spec.req.ProposalId,
})
s.Require().NoError(err)
votesByProposal := votesByProposalRes.Votes
s.Require().Equal(1, len(votesByProposal))
vote := votesByProposal[0]
s.Assert().Equal(spec.req.ProposalId, vote.ProposalId)
s.Assert().Equal(spec.req.Voter, vote.Voter)
s.Assert().Equal(spec.req.Choice, vote.Choice)
s.Assert().Equal(spec.req.Metadata, vote.Metadata)
vsubmittedAt, err := gogotypes.TimestampProto(vote.SubmittedAt)
s.Require().NoError(err)
submittedAt, err = gogotypes.TimestampFromProto(vsubmittedAt)
s.Require().NoError(err)
s.Assert().Equal(s.blockTime, submittedAt)
// query votes by voter
voter := spec.req.Voter
votesByVoterRes, err := s.keeper.VotesByVoter(ctx, &group.QueryVotesByVoterRequest{
Voter: voter,
})
s.Require().NoError(err)
votesByVoter := votesByVoterRes.Votes
s.Require().Equal(1, len(votesByVoter))
s.Assert().Equal(spec.req.ProposalId, votesByVoter[0].ProposalId)
s.Assert().Equal(voter, votesByVoter[0].Voter)
s.Assert().Equal(spec.req.Choice, votesByVoter[0].Choice)
s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata)
vsubmittedAt, err = gogotypes.TimestampProto(votesByVoter[0].SubmittedAt)
s.Require().NoError(err)
submittedAt, err = gogotypes.TimestampFromProto(vsubmittedAt)
s.Require().NoError(err)
s.Assert().Equal(s.blockTime, submittedAt)
// and proposal is updated
proposalRes, err := s.keeper.Proposal(ctx, &group.QueryProposalRequest{
ProposalId: spec.req.ProposalId,
})
s.Require().NoError(err)
proposal := proposalRes.Proposal
s.Assert().Equal(spec.expVoteState, proposal.VoteState)
s.Assert().Equal(spec.expResult, proposal.Result)
s.Assert().Equal(spec.expProposalStatus, proposal.Status)
s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult)
spec.postRun(sdkCtx)
})
}
}
func (s *TestSuite) TestExecProposal() {
addrs := s.addrs
addr1 := addrs[0]
addr2 := addrs[1]
msgSend1 := &banktypes.MsgSend{
FromAddress: s.groupAccountAddr.String(),
ToAddress: addr2.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)},
}
msgSend2 := &banktypes.MsgSend{
FromAddress: s.groupAccountAddr.String(),
ToAddress: addr2.String(),
Amount: sdk.Coins{sdk.NewInt64Coin("test", 10001)},
}
proposers := []string{addr2.String()}
specs := map[string]struct {
srcBlockTime time.Time
setupProposal func(ctx context.Context) uint64
expErr bool
expProposalStatus group.Proposal_Status
expProposalResult group.Proposal_Result
expExecutorResult group.Proposal_ExecutorResult
expBalance bool
expFromBalances sdk.Coin
expToBalances sdk.Coin
}{
"proposal executed when accepted": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_YES)
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultSuccess,
expBalance: true,
expFromBalances: sdk.NewInt64Coin("test", 9900),
expToBalances: sdk.NewInt64Coin("test", 100),
},
"proposal with multiple messages executed when accepted": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1, msgSend1}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_YES)
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultSuccess,
expBalance: true,
expFromBalances: sdk.NewInt64Coin("test", 9800),
expToBalances: sdk.NewInt64Coin("test", 200),
},
"proposal not executed when rejected": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_NO)
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultRejected,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"open proposal must not fail": {
setupProposal: func(ctx context.Context) uint64 {
return createProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
},
expProposalStatus: group.ProposalStatusSubmitted,
expProposalResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"existing proposal required": {
setupProposal: func(ctx context.Context) uint64 {
return 9999
},
expErr: true,
},
"Decision policy also applied on timeout": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_NO)
},
srcBlockTime: s.blockTime.Add(time.Second),
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultRejected,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"Decision policy also applied after timeout": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_NO)
},
srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond),
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultRejected,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"with group modified before tally": {
setupProposal: func(ctx context.Context) uint64 {
myProposalID := createProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
// then modify group
_, err := s.keeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{
Admin: addr1.String(),
GroupId: s.groupID,
Metadata: []byte{1, 2, 3},
})
s.Require().NoError(err)
return myProposalID
},
expProposalStatus: group.ProposalStatusAborted,
expProposalResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"with group account modified before tally": {
setupProposal: func(ctx context.Context) uint64 {
myProposalID := createProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
_, err := s.keeper.UpdateGroupAccountMetadata(ctx, &group.MsgUpdateGroupAccountMetadata{
Admin: addr1.String(),
Address: s.groupAccountAddr.String(),
Metadata: []byte("group account modified before tally"),
})
s.Require().NoError(err)
return myProposalID
},
expProposalStatus: group.ProposalStatusAborted,
expProposalResult: group.ProposalResultUnfinalized,
expExecutorResult: group.ProposalExecutorResultNotRun,
},
"prevent double execution when successful": {
setupProposal: func(ctx context.Context) uint64 {
myProposalID := createProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.Choice_CHOICE_YES)
_, err := s.keeper.Exec(ctx, &group.MsgExec{Signer: addr1.String(), ProposalId: myProposalID})
s.Require().NoError(err)
return myProposalID
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultSuccess,
expBalance: true,
expFromBalances: sdk.NewInt64Coin("test", 9900),
expToBalances: sdk.NewInt64Coin("test", 100),
},
"rollback all msg updates on failure": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend1, msgSend2}
return createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_YES)
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultFailure,
},
"executable when failed before": {
setupProposal: func(ctx context.Context) uint64 {
msgs := []sdk.Msg{msgSend2}
myProposalID := createProposalAndVote(ctx, s, msgs, proposers, group.Choice_CHOICE_YES)
_, err := s.keeper.Exec(ctx, &group.MsgExec{Signer: addr1.String(), ProposalId: myProposalID})
s.Require().NoError(err)
sdkCtx := sdk.UnwrapSDKContext(ctx)
s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, sdkCtx, s.groupAccountAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)}))
return myProposalID
},
expProposalStatus: group.ProposalStatusClosed,
expProposalResult: group.ProposalResultAccepted,
expExecutorResult: group.ProposalExecutorResultSuccess,
},
}
for msg, spec := range specs {
spec := spec
s.Run(msg, func() {
sdkCtx, _ := s.sdkCtx.CacheContext()
ctx := sdk.WrapSDKContext(sdkCtx)
proposalID := spec.setupProposal(ctx)
if !spec.srcBlockTime.IsZero() {
sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime)
}
ctx = sdk.WrapSDKContext(sdkCtx)
_, err := s.keeper.Exec(ctx, &group.MsgExec{Signer: addr1.String(), ProposalId: proposalID})
if spec.expErr {
s.Require().Error(err)
return
}
s.Require().NoError(err)
// and proposal is updated
res, err := s.keeper.Proposal(ctx, &group.QueryProposalRequest{ProposalId: proposalID})
s.Require().NoError(err)
proposal := res.Proposal
exp := group.Proposal_Result_name[int32(spec.expProposalResult)]
got := group.Proposal_Result_name[int32(proposal.Result)]
s.Assert().Equal(exp, got)
exp = group.Proposal_Status_name[int32(spec.expProposalStatus)]
got = group.Proposal_Status_name[int32(proposal.Status)]
s.Assert().Equal(exp, got)
exp = group.Proposal_ExecutorResult_name[int32(spec.expExecutorResult)]
got = group.Proposal_ExecutorResult_name[int32(proposal.ExecutorResult)]
s.Assert().Equal(exp, got)
if spec.expBalance {
fromBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, s.groupAccountAddr)
s.Require().Contains(fromBalances, spec.expFromBalances)
toBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, addr2)
s.Require().Contains(toBalances, spec.expToBalances)
}
})
}
}
func createProposal(
ctx context.Context, s *TestSuite, msgs []sdk.Msg,
proposers []string) uint64 {
proposalReq := &group.MsgCreateProposal{
Address: s.groupAccountAddr.String(),
Proposers: proposers,
Metadata: nil,
}
err := proposalReq.SetMsgs(msgs)
s.Require().NoError(err)
proposalRes, err := s.keeper.CreateProposal(ctx, proposalReq)
s.Require().NoError(err)
return proposalRes.ProposalId
}
func createProposalAndVote(
ctx context.Context, s *TestSuite, msgs []sdk.Msg,
proposers []string, choice group.Choice) uint64 {
s.Require().Greater(len(proposers), 0)
myProposalID := createProposal(ctx, s, msgs, proposers)
_, err := s.keeper.Vote(ctx, &group.MsgVote{
ProposalId: myProposalID,
Voter: proposers[0],
Choice: choice,
})
s.Require().NoError(err)
return myProposalID
}
func createGroupAndGroupAccount(
admin sdk.AccAddress,
s *TestSuite,
) (string, uint64, group.DecisionPolicy) {
groupRes, err := s.keeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
Admin: admin.String(),
Members: nil,
Metadata: nil,
})
s.Require().NoError(err)
myGroupID := groupRes.GroupId
groupAccount := &group.MsgCreateGroupAccount{
Admin: admin.String(),
GroupId: myGroupID,
Metadata: nil,
}
policy := group.NewThresholdDecisionPolicy(
"1",
time.Second,
)
err = groupAccount.SetDecisionPolicy(policy)
s.Require().NoError(err)
groupAccountRes, err := s.keeper.CreateGroupAccount(s.ctx, groupAccount)
s.Require().NoError(err)
return groupAccountRes.Address, myGroupID, policy
}