From 615c08772943bb81117b59b0e7db74212b2f38a0 Mon Sep 17 00:00:00 2001 From: Jeancarlo Barrios Date: Fri, 26 May 2023 04:43:17 -0500 Subject: [PATCH] feat(group): add group event tally result (#16191) Co-authored-by: Julien Robert (cherry picked from commit be2003e58101cd506eb1249b1e0435accf5a4b58) # Conflicts: # CHANGELOG.md # api/cosmos/group/v1/types.pulsar.go # proto/cosmos/group/v1/types.proto # x/group/keeper/keeper.go # x/group/keeper/msg_server.go # x/group/keeper/msg_server_test.go # x/group/types.pb.go --- CHANGELOG.md | 36 + api/cosmos/group/v1/events.pulsar.go | 656 ++++- api/cosmos/group/v1/types.pulsar.go | 10 + proto/cosmos/group/v1/events.proto | 13 + proto/cosmos/group/v1/query.proto | 6 +- proto/cosmos/group/v1/types.proto | 10 + x/group/README.md | 10 + x/group/events.pb.go | 307 ++- x/group/keeper/genesis.go | 2 +- x/group/keeper/keeper.go | 25 + x/group/keeper/msg_server.go | 15 + x/group/keeper/msg_server_test.go | 3380 ++++++++++++++++++++++++++ x/group/types.pb.go | 10 + 13 files changed, 4430 insertions(+), 50 deletions(-) create mode 100644 x/group/keeper/msg_server_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5eb0c7507a..a233a104c551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,42 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +<<<<<<< HEAD +======= +### Features + +* (types) [#15958](https://github.com/cosmos/cosmos-sdk/pull/15958) Add `module.NewBasicManagerFromManager` for creating a basic module manager from a module manager. +* (runtime) [#15818](https://github.com/cosmos/cosmos-sdk/pull/15818) Provide logger through `depinject` instead of appBuilder. +* (client) [#15597](https://github.com/cosmos/cosmos-sdk/pull/15597) Add status endpoint for clients. +* (testutil/integration) [#15556](https://github.com/cosmos/cosmos-sdk/pull/15556) Introduce `testutil/integration` package for module integration testing. +* (types) [#15735](https://github.com/cosmos/cosmos-sdk/pull/15735) Make `ValidateBasic() error` method of `Msg` interface optional. Modules should validate messages directly in their message handlers ([RFC 001](https://docs.cosmos.network/main/rfc/rfc-001-tx-validation)). +* (x/genutil) [#15679](https://github.com/cosmos/cosmos-sdk/pull/15679) Allow applications to specify a custom genesis migration function for the `genesis migrate` command. +* (client) [#15458](https://github.com/cosmos/cosmos-sdk/pull/15458) Add a `CmdContext` field to client.Context initialized to cobra command's context. +* (core) [#15133](https://github.com/cosmos/cosmos-sdk/pull/15133) Implement RegisterServices in the module manager. +* (x/gov) [#14373](https://github.com/cosmos/cosmos-sdk/pull/14373) Add new proto field `constitution` of type `string` to gov module genesis state, which allows chain builders to lay a strong foundation by specifying purpose. +* (x/genutil) [#15301](https://github.com/cosmos/cosmos-sdk/pull/15031) Add application genesis. The genesis is now entirely managed by the application and passed to CometBFT at note instantiation. Functions that were taking a `cmttypes.GenesisDoc{}` now takes a `genutiltypes.AppGenesis{}`. +* (cli) [#14659](https://github.com/cosmos/cosmos-sdk/pull/14659) Added ability to query blocks by events with queries directly passed to Tendermint, which will allow for full query operator support, e.g. `>`. +* (x/gov) [#14720](https://github.com/cosmos/cosmos-sdk/pull/14720) Upstream expedited proposals from Osmosis. +* (x/auth) [#14650](https://github.com/cosmos/cosmos-sdk/pull/14650) Add Textual SignModeHandler. It is however **NOT** enabled by default, and should only be used for **TESTING** purposes until `SIGN_MODE_TEXTUAL` is fully released. +* (x/crisis) [#14588](https://github.com/cosmos/cosmos-sdk/pull/14588) Use CacheContext() in AssertInvariants() +* (client) [#14342](https://github.com/cosmos/cosmos-sdk/pull/14342) Add ` config` command is now a sub-command, for setting, getting and migrating Cosmos SDK configuration files. +* (query) [#14468](https://github.com/cosmos/cosmos-sdk/pull/14468) Implement pagination for collections. +* (x/distribution) [#14322](https://github.com/cosmos/cosmos-sdk/pull/14322) Introduce a new gRPC message handler, `DepositValidatorRewardsPool`, that allows explicit funding of a validator's reward pool. +* [#13473](https://github.com/cosmos/cosmos-sdk/pull/13473) ADR-038: Go plugin system proposal +* (mempool) [#14484](https://github.com/cosmos/cosmos-sdk/pull/14484) Add priority nonce mempool option for transaction replacement. +* (x/bank) [#14894](https://github.com/cosmos/cosmos-sdk/pull/14894) Return a human readable denomination for IBC vouchers when querying bank balances. Added a `ResolveDenom` parameter to `types.QueryAllBalancesRequest` and `--resolve-denom` flag to `GetBalancesCmd()`. +* (x/gov) [#15151](https://github.com/cosmos/cosmos-sdk/pull/15151) Add `burn_vote_quorum`, `burn_proposal_deposit_prevote` and `burn_vote_veto` params to allow applications to decide if they would like to burn deposits +* (runtime) [#15547](https://github.com/cosmos/cosmos-sdk/pull/15547) Allow runtime to pass event core api service to modules +* (telemetry) [#15657](https://github.com/cosmos/cosmos-sdk/pull/15657) Emit more data (go version, sdk version, upgrade height) in prom metrics +* (modulemanager) [#15829](https://github.com/cosmos/cosmos-sdk/pull/15829) add new endblocker interface to handle valset updates +* (core) [#14860](https://github.com/cosmos/cosmos-sdk/pull/14860) Add `Precommit` and `PrepareCheckState` AppModule callbacks. +* (tx) [#15992](https://github.com/cosmos/cosmos-sdk/pull/15992) Add `WithExtensionOptions` in tx Factory to allow `SetExtensionOptions` with given extension options. +* (types/simulation) [#16074](https://github.com/cosmos/cosmos-sdk/pull/16074) Add generic SimulationStoreDecoder for modules using collections. +* (cli) [#16209](https://github.com/cosmos/cosmos-sdk/pull/16209) Make `StartCmd` more customizable. +* (types) [#16257](https://github.com/cosmos/cosmos-sdk/pull/16257) Allow setting the base denom in the denom registry. +* (x/group) [#16191](https://github.com/cosmos/cosmos-sdk/pull/16191) Add EventProposalPruned event to group module whenever a proposal is pruned. + +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) ### Improvements * (deps) [#16083](https://github.com/cosmos/cosmos-sdk/pull/16083) Bumps `proto-builder` image to 0.13.0. diff --git a/api/cosmos/group/v1/events.pulsar.go b/api/cosmos/group/v1/events.pulsar.go index 5352013f75c7..f800c7d979b5 100644 --- a/api/cosmos/group/v1/events.pulsar.go +++ b/api/cosmos/group/v1/events.pulsar.go @@ -3857,6 +3857,537 @@ func (x *fastReflection_EventLeaveGroup) ProtoMethods() *protoiface.Methods { } } +var ( + md_EventProposalPruned protoreflect.MessageDescriptor + fd_EventProposalPruned_proposal_id protoreflect.FieldDescriptor + fd_EventProposalPruned_status protoreflect.FieldDescriptor + fd_EventProposalPruned_tally_result protoreflect.FieldDescriptor +) + +func init() { + file_cosmos_group_v1_events_proto_init() + md_EventProposalPruned = File_cosmos_group_v1_events_proto.Messages().ByName("EventProposalPruned") + fd_EventProposalPruned_proposal_id = md_EventProposalPruned.Fields().ByName("proposal_id") + fd_EventProposalPruned_status = md_EventProposalPruned.Fields().ByName("status") + fd_EventProposalPruned_tally_result = md_EventProposalPruned.Fields().ByName("tally_result") +} + +var _ protoreflect.Message = (*fastReflection_EventProposalPruned)(nil) + +type fastReflection_EventProposalPruned EventProposalPruned + +func (x *EventProposalPruned) ProtoReflect() protoreflect.Message { + return (*fastReflection_EventProposalPruned)(x) +} + +func (x *EventProposalPruned) slowProtoReflect() protoreflect.Message { + mi := &file_cosmos_group_v1_events_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_EventProposalPruned_messageType fastReflection_EventProposalPruned_messageType +var _ protoreflect.MessageType = fastReflection_EventProposalPruned_messageType{} + +type fastReflection_EventProposalPruned_messageType struct{} + +func (x fastReflection_EventProposalPruned_messageType) Zero() protoreflect.Message { + return (*fastReflection_EventProposalPruned)(nil) +} +func (x fastReflection_EventProposalPruned_messageType) New() protoreflect.Message { + return new(fastReflection_EventProposalPruned) +} +func (x fastReflection_EventProposalPruned_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_EventProposalPruned +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_EventProposalPruned) Descriptor() protoreflect.MessageDescriptor { + return md_EventProposalPruned +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_EventProposalPruned) Type() protoreflect.MessageType { + return _fastReflection_EventProposalPruned_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_EventProposalPruned) New() protoreflect.Message { + return new(fastReflection_EventProposalPruned) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_EventProposalPruned) Interface() protoreflect.ProtoMessage { + return (*EventProposalPruned)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_EventProposalPruned) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.ProposalId != uint64(0) { + value := protoreflect.ValueOfUint64(x.ProposalId) + if !f(fd_EventProposalPruned_proposal_id, value) { + return + } + } + if x.Status != 0 { + value := protoreflect.ValueOfEnum((protoreflect.EnumNumber)(x.Status)) + if !f(fd_EventProposalPruned_status, value) { + return + } + } + if x.TallyResult != nil { + value := protoreflect.ValueOfMessage(x.TallyResult.ProtoReflect()) + if !f(fd_EventProposalPruned_tally_result, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_EventProposalPruned) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "cosmos.group.v1.EventProposalPruned.proposal_id": + return x.ProposalId != uint64(0) + case "cosmos.group.v1.EventProposalPruned.status": + return x.Status != 0 + case "cosmos.group.v1.EventProposalPruned.tally_result": + return x.TallyResult != nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_EventProposalPruned) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "cosmos.group.v1.EventProposalPruned.proposal_id": + x.ProposalId = uint64(0) + case "cosmos.group.v1.EventProposalPruned.status": + x.Status = 0 + case "cosmos.group.v1.EventProposalPruned.tally_result": + x.TallyResult = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_EventProposalPruned) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "cosmos.group.v1.EventProposalPruned.proposal_id": + value := x.ProposalId + return protoreflect.ValueOfUint64(value) + case "cosmos.group.v1.EventProposalPruned.status": + value := x.Status + return protoreflect.ValueOfEnum((protoreflect.EnumNumber)(value)) + case "cosmos.group.v1.EventProposalPruned.tally_result": + value := x.TallyResult + return protoreflect.ValueOfMessage(value.ProtoReflect()) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_EventProposalPruned) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "cosmos.group.v1.EventProposalPruned.proposal_id": + x.ProposalId = value.Uint() + case "cosmos.group.v1.EventProposalPruned.status": + x.Status = (ProposalStatus)(value.Enum()) + case "cosmos.group.v1.EventProposalPruned.tally_result": + x.TallyResult = value.Message().Interface().(*TallyResult) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_EventProposalPruned) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "cosmos.group.v1.EventProposalPruned.tally_result": + if x.TallyResult == nil { + x.TallyResult = new(TallyResult) + } + return protoreflect.ValueOfMessage(x.TallyResult.ProtoReflect()) + case "cosmos.group.v1.EventProposalPruned.proposal_id": + panic(fmt.Errorf("field proposal_id of message cosmos.group.v1.EventProposalPruned is not mutable")) + case "cosmos.group.v1.EventProposalPruned.status": + panic(fmt.Errorf("field status of message cosmos.group.v1.EventProposalPruned is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_EventProposalPruned) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "cosmos.group.v1.EventProposalPruned.proposal_id": + return protoreflect.ValueOfUint64(uint64(0)) + case "cosmos.group.v1.EventProposalPruned.status": + return protoreflect.ValueOfEnum(0) + case "cosmos.group.v1.EventProposalPruned.tally_result": + m := new(TallyResult) + return protoreflect.ValueOfMessage(m.ProtoReflect()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned")) + } + panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_EventProposalPruned) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in cosmos.group.v1.EventProposalPruned", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_EventProposalPruned) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_EventProposalPruned) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_EventProposalPruned) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_EventProposalPruned) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*EventProposalPruned) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.ProposalId != 0 { + n += 1 + runtime.Sov(uint64(x.ProposalId)) + } + if x.Status != 0 { + n += 1 + runtime.Sov(uint64(x.Status)) + } + if x.TallyResult != nil { + l = options.Size(x.TallyResult) + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*EventProposalPruned) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if x.TallyResult != nil { + encoded, err := options.Marshal(x.TallyResult) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0x1a + } + if x.Status != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.Status)) + i-- + dAtA[i] = 0x10 + } + if x.ProposalId != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.ProposalId)) + i-- + dAtA[i] = 0x8 + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*EventProposalPruned) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: EventProposalPruned: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: EventProposalPruned: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType) + } + x.ProposalId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.ProposalId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Status", wireType) + } + x.Status = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.Status |= ProposalStatus(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TallyResult", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.TallyResult == nil { + x.TallyResult = &TallyResult{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.TallyResult); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + // Since: cosmos-sdk 0.46 // Code generated by protoc-gen-go. DO NOT EDIT. @@ -4232,6 +4763,61 @@ func (x *EventLeaveGroup) GetAddress() string { return "" } +// EventProposalTallyFinalized is an event emitted when a proposal tally is finalized. +type EventProposalPruned struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // proposal_id is the unique ID of the proposal. + ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty"` + // status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN). + Status ProposalStatus `protobuf:"varint,2,opt,name=status,proto3,enum=cosmos.group.v1.ProposalStatus" json:"status,omitempty"` + // tally_result is the proposal tally result. + TallyResult *TallyResult `protobuf:"bytes,3,opt,name=tally_result,json=tallyResult,proto3" json:"tally_result,omitempty"` +} + +func (x *EventProposalPruned) Reset() { + *x = EventProposalPruned{} + if protoimpl.UnsafeEnabled { + mi := &file_cosmos_group_v1_events_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EventProposalPruned) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventProposalPruned) ProtoMessage() {} + +// Deprecated: Use EventProposalPruned.ProtoReflect.Descriptor instead. +func (*EventProposalPruned) Descriptor() ([]byte, []int) { + return file_cosmos_group_v1_events_proto_rawDescGZIP(), []int{9} +} + +func (x *EventProposalPruned) GetProposalId() uint64 { + if x != nil { + return x.ProposalId + } + return 0 +} + +func (x *EventProposalPruned) GetStatus() ProposalStatus { + if x != nil { + return x.Status + } + return ProposalStatus_PROPOSAL_STATUS_UNSPECIFIED +} + +func (x *EventProposalPruned) GetTallyResult() *TallyResult { + if x != nil { + return x.TallyResult + } + return nil +} + var File_cosmos_group_v1_events_proto protoreflect.FileDescriptor var file_cosmos_group_v1_events_proto_rawDesc = []byte{ @@ -4281,18 +4867,29 @@ var file_cosmos_group_v1_events_proto_rawDesc = []byte{ 0x70, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0xaa, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, - 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x76, 0x31, 0x42, - 0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, - 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2f, 0x76, 0x31, - 0x3b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47, 0x58, 0xaa, 0x02, - 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x56, 0x31, - 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x5c, - 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xb0, 0x01, 0x0a, 0x13, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x49, 0x64, + 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3f, 0x0a, 0x0c, 0x74, 0x61, 0x6c, + 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x61, 0x6c, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0b, 0x74, + 0x61, 0x6c, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0xaa, 0x01, 0x0a, 0x13, 0x63, + 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, + 0x76, 0x31, 0x42, 0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x28, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47, + 0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4307,7 +4904,7 @@ func file_cosmos_group_v1_events_proto_rawDescGZIP() []byte { return file_cosmos_group_v1_events_proto_rawDescData } -var file_cosmos_group_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_cosmos_group_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_cosmos_group_v1_events_proto_goTypes = []interface{}{ (*EventCreateGroup)(nil), // 0: cosmos.group.v1.EventCreateGroup (*EventUpdateGroup)(nil), // 1: cosmos.group.v1.EventUpdateGroup @@ -4318,15 +4915,20 @@ var file_cosmos_group_v1_events_proto_goTypes = []interface{}{ (*EventVote)(nil), // 6: cosmos.group.v1.EventVote (*EventExec)(nil), // 7: cosmos.group.v1.EventExec (*EventLeaveGroup)(nil), // 8: cosmos.group.v1.EventLeaveGroup - (ProposalExecutorResult)(0), // 9: cosmos.group.v1.ProposalExecutorResult + (*EventProposalPruned)(nil), // 9: cosmos.group.v1.EventProposalPruned + (ProposalExecutorResult)(0), // 10: cosmos.group.v1.ProposalExecutorResult + (ProposalStatus)(0), // 11: cosmos.group.v1.ProposalStatus + (*TallyResult)(nil), // 12: cosmos.group.v1.TallyResult } var file_cosmos_group_v1_events_proto_depIdxs = []int32{ - 9, // 0: cosmos.group.v1.EventExec.result:type_name -> cosmos.group.v1.ProposalExecutorResult - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 10, // 0: cosmos.group.v1.EventExec.result:type_name -> cosmos.group.v1.ProposalExecutorResult + 11, // 1: cosmos.group.v1.EventProposalPruned.status:type_name -> cosmos.group.v1.ProposalStatus + 12, // 2: cosmos.group.v1.EventProposalPruned.tally_result:type_name -> cosmos.group.v1.TallyResult + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_cosmos_group_v1_events_proto_init() } @@ -4444,6 +5046,18 @@ func file_cosmos_group_v1_events_proto_init() { return nil } } + file_cosmos_group_v1_events_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EventProposalPruned); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -4451,7 +5065,7 @@ func file_cosmos_group_v1_events_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_cosmos_group_v1_events_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/api/cosmos/group/v1/types.pulsar.go b/api/cosmos/group/v1/types.pulsar.go index bc96cac970a4..379659cdd1e6 100644 --- a/api/cosmos/group/v1/types.pulsar.go +++ b/api/cosmos/group/v1/types.pulsar.go @@ -7965,6 +7965,11 @@ type GroupPolicyInfo struct { // admin is the account address of the group admin. Admin string `protobuf:"bytes,3,opt,name=admin,proto3" json:"admin,omitempty"` // metadata is any arbitrary metadata attached to the group policy. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#decision-policy-1 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) Metadata string `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` // version is used to track changes to a group's GroupPolicyInfo structure that // would create a different result on a running proposal. @@ -8058,6 +8063,11 @@ type Proposal struct { // group_policy_address is the account address of group policy. GroupPolicyAddress string `protobuf:"bytes,2,opt,name=group_policy_address,json=groupPolicyAddress,proto3" json:"group_policy_address,omitempty"` // metadata is any arbitrary metadata attached to the proposal. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#proposal-4 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` // proposers are the account addresses of the proposers. Proposers []string `protobuf:"bytes,4,rep,name=proposers,proto3" json:"proposers,omitempty"` diff --git a/proto/cosmos/group/v1/events.proto b/proto/cosmos/group/v1/events.proto index c2cfe8728f72..2b98ec9abc32 100644 --- a/proto/cosmos/group/v1/events.proto +++ b/proto/cosmos/group/v1/events.proto @@ -79,3 +79,16 @@ message EventLeaveGroup { // address is the account address of the group member. string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } + +// EventProposalPruned is an event emitted when a proposal is pruned. +message EventProposalPruned { + + // proposal_id is the unique ID of the proposal. + uint64 proposal_id = 1; + + // status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN). + ProposalStatus status = 2; + + // tally_result is the proposal tally result (when applicable). + TallyResult tally_result = 3; +} diff --git a/proto/cosmos/group/v1/query.proto b/proto/cosmos/group/v1/query.proto index f141d26b9352..80b09255afed 100644 --- a/proto/cosmos/group/v1/query.proto +++ b/proto/cosmos/group/v1/query.proto @@ -85,7 +85,7 @@ service Query { }; // Groups queries all groups in state. - // + // // Since: cosmos-sdk 0.47.1 rpc Groups(QueryGroupsRequest) returns (QueryGroupsResponse) { option (google.api.http).get = "/cosmos/group/v1/groups"; @@ -300,7 +300,7 @@ message QueryTallyResultResponse { } // QueryGroupsRequest is the Query/Groups request type. -// +// // Since: cosmos-sdk 0.47.1 message QueryGroupsRequest { @@ -309,7 +309,7 @@ message QueryGroupsRequest { } // QueryGroupsResponse is the Query/Groups response type. -// +// // Since: cosmos-sdk 0.47.1 message QueryGroupsResponse { // `groups` is all the groups present in state. diff --git a/proto/cosmos/group/v1/types.proto b/proto/cosmos/group/v1/types.proto index 99838401ead8..8d6358e77240 100644 --- a/proto/cosmos/group/v1/types.proto +++ b/proto/cosmos/group/v1/types.proto @@ -171,6 +171,11 @@ message GroupPolicyInfo { string admin = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // metadata is any arbitrary metadata attached to the group policy. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#decision-policy-1 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) string metadata = 4; // version is used to track changes to a group's GroupPolicyInfo structure that @@ -199,6 +204,11 @@ message Proposal { string group_policy_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // metadata is any arbitrary metadata attached to the proposal. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#proposal-4 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) string metadata = 3; // proposers are the account addresses of the proposers. diff --git a/x/group/README.md b/x/group/README.md index b94fec728650..29e5478b5fb2 100644 --- a/x/group/README.md +++ b/x/group/README.md @@ -49,6 +49,7 @@ This module allows the creation and management of on-chain multisig accounts and * [EventVote](#eventvote) * [EventExec](#eventexec) * [EventLeaveGroup](#eventleavegroup) + * [EventProposalPruned](#eventproposalpruned) * [Client](#client) * [CLI](#cli) * [gRPC](#grpc) @@ -578,6 +579,15 @@ The group module emits the following events: | cosmos.group.v1.EventLeaveGroup | proposal_id | {proposalId} | | cosmos.group.v1.EventLeaveGroup | address | {address} | +### EventProposalPruned + +| Type | Attribute Key | Attribute Value | +|-------------------------------------|---------------|---------------------------------| +| message | action | /cosmos.group.v1.Msg/LeaveGroup | +| cosmos.group.v1.EventProposalPruned | proposal_id | {proposalId} | +| cosmos.group.v1.EventProposalPruned | status | {ProposalStatus} | +| cosmos.group.v1.EventProposalPruned | tally_result | {TallyResult} | + ## Client diff --git a/x/group/events.pb.go b/x/group/events.pb.go index 1e30b1f06c9d..9654898cbe2d 100644 --- a/x/group/events.pb.go +++ b/x/group/events.pb.go @@ -464,6 +464,70 @@ func (m *EventLeaveGroup) GetAddress() string { return "" } +// EventProposalTallyFinalized is an event emitted when a proposal tally is finalized. +type EventProposalPruned struct { + // proposal_id is the unique ID of the proposal. + ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty"` + // status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN). + Status ProposalStatus `protobuf:"varint,2,opt,name=status,proto3,enum=cosmos.group.v1.ProposalStatus" json:"status,omitempty"` + // tally_result is the proposal tally result. + TallyResult *TallyResult `protobuf:"bytes,3,opt,name=tally_result,json=tallyResult,proto3" json:"tally_result,omitempty"` +} + +func (m *EventProposalPruned) Reset() { *m = EventProposalPruned{} } +func (m *EventProposalPruned) String() string { return proto.CompactTextString(m) } +func (*EventProposalPruned) ProtoMessage() {} +func (*EventProposalPruned) Descriptor() ([]byte, []int) { + return fileDescriptor_e8d753981546f032, []int{9} +} +func (m *EventProposalPruned) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EventProposalPruned) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EventProposalPruned.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EventProposalPruned) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventProposalPruned.Merge(m, src) +} +func (m *EventProposalPruned) XXX_Size() int { + return m.Size() +} +func (m *EventProposalPruned) XXX_DiscardUnknown() { + xxx_messageInfo_EventProposalPruned.DiscardUnknown(m) +} + +var xxx_messageInfo_EventProposalPruned proto.InternalMessageInfo + +func (m *EventProposalPruned) GetProposalId() uint64 { + if m != nil { + return m.ProposalId + } + return 0 +} + +func (m *EventProposalPruned) GetStatus() ProposalStatus { + if m != nil { + return m.Status + } + return PROPOSAL_STATUS_UNSPECIFIED +} + +func (m *EventProposalPruned) GetTallyResult() *TallyResult { + if m != nil { + return m.TallyResult + } + return nil +} + func init() { proto.RegisterType((*EventCreateGroup)(nil), "cosmos.group.v1.EventCreateGroup") proto.RegisterType((*EventUpdateGroup)(nil), "cosmos.group.v1.EventUpdateGroup") @@ -474,36 +538,41 @@ func init() { proto.RegisterType((*EventVote)(nil), "cosmos.group.v1.EventVote") proto.RegisterType((*EventExec)(nil), "cosmos.group.v1.EventExec") proto.RegisterType((*EventLeaveGroup)(nil), "cosmos.group.v1.EventLeaveGroup") + proto.RegisterType((*EventProposalPruned)(nil), "cosmos.group.v1.EventProposalPruned") } func init() { proto.RegisterFile("cosmos/group/v1/events.proto", fileDescriptor_e8d753981546f032) } var fileDescriptor_e8d753981546f032 = []byte{ - // 382 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x49, 0xce, 0x2f, 0xce, - 0xcd, 0x2f, 0xd6, 0x4f, 0x2f, 0xca, 0x2f, 0x2d, 0xd0, 0x2f, 0x33, 0xd4, 0x4f, 0x2d, 0x4b, 0xcd, - 0x2b, 0x29, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x87, 0xc8, 0xea, 0x81, 0x65, 0xf5, - 0xca, 0x0c, 0xa5, 0x24, 0x21, 0x02, 0xf1, 0x60, 0x69, 0x7d, 0xa8, 0x2c, 0x98, 0x23, 0x25, 0x8d, - 0x6e, 0x52, 0x49, 0x65, 0x41, 0x2a, 0x54, 0x52, 0x49, 0x97, 0x4b, 0xc0, 0x15, 0x64, 0xb0, 0x73, - 0x51, 0x6a, 0x62, 0x49, 0xaa, 0x3b, 0x48, 0x89, 0x90, 0x24, 0x17, 0x07, 0x58, 0x6d, 0x7c, 0x66, - 0x8a, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x3b, 0x98, 0xef, 0x99, 0x02, 0x57, 0x1e, 0x5a, - 0x90, 0x42, 0x8c, 0x72, 0x1f, 0x2e, 0x31, 0x74, 0xd3, 0x03, 0xf2, 0x73, 0x32, 0x93, 0x2b, 0x85, - 0x8c, 0xb8, 0xd8, 0x13, 0x53, 0x52, 0x8a, 0x52, 0x8b, 0x8b, 0xc1, 0x7a, 0x38, 0x9d, 0x24, 0x2e, - 0x6d, 0xd1, 0x15, 0x81, 0xba, 0xdb, 0x11, 0x22, 0x13, 0x5c, 0x52, 0x94, 0x99, 0x97, 0x1e, 0x04, - 0x53, 0x08, 0x37, 0x0d, 0xc9, 0x72, 0x0a, 0x4c, 0x33, 0xe3, 0x12, 0x06, 0x9b, 0x16, 0x5c, 0x9a, - 0x94, 0x9b, 0x59, 0x12, 0x50, 0x94, 0x5f, 0x90, 0x5f, 0x9c, 0x98, 0x23, 0x24, 0xcf, 0xc5, 0x5d, - 0x00, 0x65, 0x23, 0x3c, 0xc4, 0x05, 0x13, 0xf2, 0x4c, 0x51, 0xb2, 0xe0, 0x12, 0x05, 0xeb, 0x0b, - 0xcf, 0x2c, 0xc9, 0x48, 0x29, 0x4a, 0x2c, 0x27, 0x5e, 0xa7, 0x0e, 0x17, 0x27, 0x58, 0x67, 0x58, - 0x7e, 0x49, 0x2a, 0x61, 0xd5, 0x8d, 0x8c, 0x50, 0xe5, 0xae, 0x15, 0xa9, 0xc9, 0x04, 0x95, 0x0b, - 0xd9, 0x73, 0xb1, 0x15, 0xa5, 0x16, 0x97, 0xe6, 0x94, 0x48, 0x30, 0x29, 0x30, 0x6a, 0xf0, 0x19, - 0xa9, 0xeb, 0xa1, 0x25, 0x11, 0x3d, 0x98, 0x43, 0x41, 0xe6, 0x95, 0x96, 0xe4, 0x17, 0x05, 0x81, - 0x95, 0x07, 0x41, 0xb5, 0x09, 0x09, 0x71, 0xb1, 0xe4, 0xe4, 0xa7, 0x17, 0x4b, 0x30, 0x83, 0x02, - 0x30, 0x08, 0xcc, 0x56, 0x4a, 0xe0, 0xe2, 0x07, 0x3b, 0xc1, 0x27, 0x35, 0xb1, 0x8c, 0x60, 0x6c, - 0x23, 0xc7, 0x02, 0x13, 0x91, 0xb1, 0xe0, 0x64, 0x77, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, - 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, - 0x72, 0x0c, 0x51, 0x2a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xd0, 0xf4, - 0x0c, 0xa5, 0x74, 0x8b, 0x53, 0xb2, 0xf5, 0x2b, 0x20, 0xc9, 0x39, 0x89, 0x0d, 0x9c, 0x8c, 0x8d, - 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5b, 0xc0, 0x98, 0xf8, 0x2f, 0x03, 0x00, 0x00, + // 442 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x93, 0x4f, 0xef, 0xd2, 0x30, + 0x18, 0xc7, 0xe9, 0x4f, 0x02, 0x52, 0x8c, 0x98, 0xfa, 0x27, 0x03, 0xc9, 0x20, 0xc4, 0x44, 0x0e, + 0xb2, 0x05, 0x4c, 0xd4, 0x93, 0x44, 0x0c, 0x31, 0x24, 0x1c, 0xc8, 0xf0, 0x4f, 0xe2, 0x05, 0xc7, + 0xda, 0x8c, 0xc5, 0x41, 0x97, 0xb6, 0x9b, 0x70, 0xf4, 0x1d, 0xf8, 0x52, 0x3c, 0xf8, 0x22, 0x3c, + 0x12, 0x4f, 0x1e, 0x0d, 0xbc, 0x11, 0xb3, 0xae, 0x03, 0x82, 0x31, 0x23, 0xf9, 0x9d, 0x68, 0xfb, + 0xfd, 0x7c, 0xbf, 0x3c, 0x4f, 0x9f, 0x15, 0xd6, 0x1d, 0xca, 0x97, 0x94, 0x9b, 0x2e, 0xa3, 0x61, + 0x60, 0x46, 0x5d, 0x93, 0x44, 0x64, 0x25, 0xb8, 0x11, 0x30, 0x2a, 0x28, 0xaa, 0x24, 0xaa, 0x21, + 0x55, 0x23, 0xea, 0xd6, 0xaa, 0xc9, 0xc1, 0x4c, 0xca, 0xa6, 0x52, 0xe5, 0xa6, 0xf6, 0xf0, 0x3c, + 0x49, 0x6c, 0x02, 0xa2, 0xc4, 0x56, 0x07, 0xde, 0x19, 0xc6, 0xc1, 0xaf, 0x19, 0xb1, 0x05, 0x79, + 0x13, 0x23, 0xa8, 0x0a, 0x6f, 0x4a, 0x76, 0xe6, 0x61, 0x0d, 0x34, 0x41, 0x3b, 0x6f, 0x15, 0xe5, + 0x7e, 0x84, 0x0f, 0xf8, 0xbb, 0x00, 0x5f, 0x82, 0x8f, 0xe1, 0x83, 0xf3, 0xf4, 0x09, 0xf5, 0x3d, + 0x67, 0x83, 0x7a, 0xb0, 0x68, 0x63, 0xcc, 0x08, 0xe7, 0xd2, 0x53, 0x1a, 0x68, 0xbf, 0x7e, 0x74, + 0xee, 0xa9, 0xba, 0x5f, 0x25, 0xca, 0x54, 0x30, 0x6f, 0xe5, 0x5a, 0x29, 0x78, 0x48, 0x3b, 0xf9, + 0xf3, 0x6b, 0xa4, 0x3d, 0x83, 0x77, 0x65, 0xda, 0x34, 0x9c, 0x2f, 0x3d, 0x31, 0x61, 0x34, 0xa0, + 0xdc, 0xf6, 0x51, 0x03, 0x96, 0x03, 0xb5, 0x3e, 0x36, 0x04, 0xd3, 0xa3, 0x11, 0x6e, 0xbd, 0x80, + 0xf7, 0xa5, 0xef, 0x83, 0x27, 0x16, 0x98, 0xd9, 0x5f, 0x2e, 0x77, 0x3e, 0x81, 0x25, 0xe9, 0x7c, + 0x4f, 0x05, 0xc9, 0xa6, 0xbf, 0x02, 0x85, 0x0f, 0xd7, 0xc4, 0xc9, 0xc4, 0x51, 0x1f, 0x16, 0x18, + 0xe1, 0xa1, 0x2f, 0xb4, 0xab, 0x26, 0x68, 0xdf, 0xee, 0x3d, 0x36, 0xce, 0x3e, 0x11, 0x23, 0x2d, + 0x34, 0xce, 0x0b, 0x05, 0x65, 0x96, 0xc4, 0x2d, 0x65, 0x43, 0x08, 0xe6, 0x7d, 0xea, 0x72, 0xed, + 0x46, 0x7c, 0x81, 0x96, 0x5c, 0xb7, 0x3e, 0xc1, 0x8a, 0x2c, 0x61, 0x4c, 0xec, 0x28, 0x73, 0xda, + 0xa7, 0x53, 0xb8, 0xba, 0x74, 0x0a, 0xdf, 0x81, 0x1a, 0x43, 0x5a, 0xdd, 0x84, 0x85, 0x2b, 0x82, + 0xb3, 0xfb, 0x7d, 0x0e, 0x0b, 0x5c, 0xd8, 0x22, 0xe4, 0xaa, 0xdf, 0xc6, 0x7f, 0xfb, 0x9d, 0x4a, + 0xcc, 0x52, 0x38, 0xea, 0xc3, 0x5b, 0xc2, 0xf6, 0xfd, 0xcd, 0x4c, 0x5d, 0x57, 0xdc, 0x6f, 0xb9, + 0x57, 0xff, 0xc7, 0xfe, 0x36, 0x86, 0xd4, 0x1d, 0x95, 0xc5, 0x71, 0x33, 0x78, 0xf9, 0x73, 0xa7, + 0x83, 0xed, 0x4e, 0x07, 0x7f, 0x76, 0x3a, 0xf8, 0xb6, 0xd7, 0x73, 0xdb, 0xbd, 0x9e, 0xfb, 0xbd, + 0xd7, 0x73, 0x1f, 0x1f, 0xb9, 0x9e, 0x58, 0x84, 0x73, 0xc3, 0xa1, 0x4b, 0xf5, 0x04, 0xd5, 0x4f, + 0x87, 0xe3, 0xcf, 0xe6, 0x3a, 0x79, 0x81, 0xf3, 0x82, 0x7c, 0x79, 0x4f, 0xff, 0x06, 0x00, 0x00, + 0xff, 0xff, 0xa5, 0x1a, 0x1c, 0xb9, 0xe2, 0x03, 0x00, 0x00, } func (m *EventCreateGroup) Marshal() (dAtA []byte, err error) { @@ -781,6 +850,51 @@ func (m *EventLeaveGroup) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *EventProposalPruned) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EventProposalPruned) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EventProposalPruned) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.TallyResult != nil { + { + size, err := m.TallyResult.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintEvents(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + if m.Status != 0 { + i = encodeVarintEvents(dAtA, i, uint64(m.Status)) + i-- + dAtA[i] = 0x10 + } + if m.ProposalId != 0 { + i = encodeVarintEvents(dAtA, i, uint64(m.ProposalId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func encodeVarintEvents(dAtA []byte, offset int, v uint64) int { offset -= sovEvents(v) base := offset @@ -913,6 +1027,25 @@ func (m *EventLeaveGroup) Size() (n int) { return n } +func (m *EventProposalPruned) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ProposalId != 0 { + n += 1 + sovEvents(uint64(m.ProposalId)) + } + if m.Status != 0 { + n += 1 + sovEvents(uint64(m.Status)) + } + if m.TallyResult != nil { + l = m.TallyResult.Size() + n += 1 + l + sovEvents(uint64(l)) + } + return n +} + func sovEvents(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -1649,6 +1782,130 @@ func (m *EventLeaveGroup) Unmarshal(dAtA []byte) error { } return nil } +func (m *EventProposalPruned) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EventProposalPruned: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EventProposalPruned: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType) + } + m.ProposalId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ProposalId |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType) + } + m.Status = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Status |= ProposalStatus(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TallyResult", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.TallyResult == nil { + m.TallyResult = &TallyResult{} + } + if err := m.TallyResult.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipEvents(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvents + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipEvents(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/group/keeper/genesis.go b/x/group/keeper/genesis.go index fd4eb13c004f..e1664b41acc0 100644 --- a/x/group/keeper/genesis.go +++ b/x/group/keeper/genesis.go @@ -44,7 +44,7 @@ func (k Keeper) InitGenesis(ctx types.Context, cdc codec.JSONCodec, data json.Ra } // ExportGenesis returns the group module's exported genesis. -func (k Keeper) ExportGenesis(ctx types.Context, cdc codec.JSONCodec) *group.GenesisState { +func (k Keeper) ExportGenesis(ctx types.Context, _ codec.JSONCodec) *group.GenesisState { genesisState := group.NewGenesisState() var groups []*group.GroupInfo diff --git a/x/group/keeper/keeper.go b/x/group/keeper/keeper.go index f8f41673cef8..5db9385ec3b9 100644 --- a/x/group/keeper/keeper.go +++ b/x/group/keeper/keeper.go @@ -4,7 +4,15 @@ import ( "fmt" "time" +<<<<<<< HEAD "github.com/cometbft/cometbft/libs/log" +======= + "cosmossdk.io/log" + + storetypes "cosmossdk.io/store/types" + + errorsmod "cosmossdk.io/errors" +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" @@ -374,6 +382,15 @@ func (k Keeper) PruneProposals(ctx sdk.Context) error { if err != nil { return err } + // Emit event for proposal finalized with its result + if err := ctx.EventManager().EmitTypedEvent( + &group.EventProposalPruned{ + ProposalId: proposal.Id, + Status: proposal.Status, + TallyResult: &proposal.FinalTallyResult, + }); err != nil { + return err + } } return nil @@ -407,6 +424,14 @@ func (k Keeper) TallyProposalsAtVPEnd(ctx sdk.Context) error { if err := k.pruneVotes(ctx, proposalID); err != nil { return err } + // Emit event for proposal finalized with its result + if err := ctx.EventManager().EmitTypedEvent( + &group.EventProposalPruned{ + ProposalId: proposal.Id, + Status: proposal.Status, + }); err != nil { + return err + } } else if proposal.Status == group.PROPOSAL_STATUS_SUBMITTED { if err := k.doTallyAndUpdate(ctx, &proposal, electorate, policyInfo); err != nil { return sdkerrors.Wrap(err, "doTallyAndUpdate") diff --git a/x/group/keeper/msg_server.go b/x/group/keeper/msg_server.go index 0112a98083e0..e15bcc4d23cb 100644 --- a/x/group/keeper/msg_server.go +++ b/x/group/keeper/msg_server.go @@ -715,6 +715,7 @@ func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate } else { p.Status = group.PROPOSAL_STATUS_REJECTED } + } return nil @@ -748,7 +749,11 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR return nil, sdkerrors.Wrap(err, "load group") } +<<<<<<< HEAD if err := k.doTallyAndUpdate(ctx, &proposal, electorate, policyInfo); err != nil { +======= + if err = k.doTallyAndUpdate(ctx, &proposal, groupInfo, policyInfo); err != nil { +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) return nil, err } } @@ -786,6 +791,16 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR if err := k.pruneProposal(ctx, proposal.Id); err != nil { return nil, err } + + // Emit event for proposal finalized with its result + if err := ctx.EventManager().EmitTypedEvent( + &group.EventProposalPruned{ + ProposalId: proposal.Id, + Status: proposal.Status, + TallyResult: &proposal.FinalTallyResult, + }); err != nil { + return nil, err + } } else { store := ctx.KVStore(k.key) if err := k.proposalTable.Update(store, id, &proposal); err != nil { diff --git a/x/group/keeper/msg_server_test.go b/x/group/keeper/msg_server_test.go new file mode 100644 index 000000000000..78ecc5226b57 --- /dev/null +++ b/x/group/keeper/msg_server_test.go @@ -0,0 +1,3380 @@ +package keeper_test + +import ( + "bytes" + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/golang/mock/gomock" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/cosmos-sdk/codec/address" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/group" + "github.com/cosmos/cosmos-sdk/x/group/internal/math" + "github.com/cosmos/cosmos-sdk/x/group/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +var EventTallyResult = "cosmos.group.v1.EventProposalPruned" + +func (s *TestSuite) TestCreateGroupWithLotsOfMembers() { + for i := 50; i < 70; i++ { + membersResp := s.createGroupAndGetMembers(i) + s.Require().Equal(len(membersResp), i) + } +} + +func (s *TestSuite) createGroupAndGetMembers(numMembers int) []*group.GroupMember { + addressPool := simtestutil.CreateIncrementalAccounts(numMembers) + members := make([]group.MemberRequest, numMembers) + for i := 0; i < len(members); i++ { + members[i] = group.MemberRequest{ + Address: addressPool[i].String(), + Weight: "1", + } + s.accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() + } + + g, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: members[0].Address, + Members: members, + }) + s.Require().NoErrorf(err, "failed to create group with %d members", len(members)) + s.T().Logf("group %d created with %d members", g.GroupId, len(members)) + + groupMemberResp, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: g.GroupId}) + s.Require().NoError(err) + + s.T().Logf("got %d members from group %d", len(groupMemberResp.Members), g.GroupId) + + return groupMemberResp.Members +} + +func (s *TestSuite) TestCreateGroup() { + addrs := s.addrs + addr1 := addrs[0] + addr3 := addrs[2] + addr5 := addrs[4] + addr6 := addrs[5] + + members := []group.MemberRequest{{ + Address: addr5.String(), + Weight: "1", + }, { + Address: addr6.String(), + Weight: "2", + }} + + expGroups := []*group.GroupInfo{ + { + Id: s.groupID, + Version: 1, + Admin: addr1.String(), + TotalWeight: "3", + CreatedAt: s.blockTime, + }, + { + Id: 2, + Version: 1, + Admin: addr1.String(), + TotalWeight: "3", + CreatedAt: s.blockTime, + }, + } + + specs := map[string]struct { + req *group.MsgCreateGroup + expErr bool + expErrMsg string + expGroups []*group.GroupInfo + }{ + "all good": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + }, + expGroups: expGroups, + }, + "group metadata too long": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "group metadata: limit exceeded", + }, + "invalid member address": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: "invalid", + Weight: "1", + }}, + }, + expErr: true, + expErrMsg: "member address invalid", + }, + "member metadata too long": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "1", + Metadata: strings.Repeat("a", 256), + }}, + }, + expErr: true, + expErrMsg: "metadata: limit exceeded", + }, + "zero member weight": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "0", + }}, + }, + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid member weight - Inf": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "inf", + }}, + }, + expErr: true, + expErrMsg: "expected a finite decimal", + }, + "invalid member weight - NaN": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "NaN", + }}, + }, + expErr: true, + expErrMsg: "expected a finite decimal", + }, + } + + var seq uint32 = 1 + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() + res, err := s.groupKeeper.CreateGroup(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + _, err := s.groupKeeper.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.groupKeeper.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.Id) + s.Assert().Equal(uint64(1), loadedGroupRes.Info.Version) + + // and members are stored as well + membersRes, err := s.groupKeeper.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(blockTime, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(id, loadedMembers[i].GroupId) + } + + // query groups by admin + groupsRes, err := s.groupKeeper.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].Id, loadedGroups[i].Id) + s.Assert().Equal(spec.expGroups[i].Version, loadedGroups[i].Version) + s.Assert().Equal(spec.expGroups[i].CreatedAt, loadedGroups[i].CreatedAt) + } + }) + } +} + +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.MemberRequest{{ + Address: member1, + Weight: "1", + }} + + myAdmin := addr4.String() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: myAdmin, + Members: members, + }) + s.Require().NoError(err) + groupID := groupRes.GroupId + + specs := map[string]struct { + req *group.MsgUpdateGroupMembers + expErr bool + expErrMsg string + expGroup *group.GroupInfo + expMembers []*group.GroupMember + }{ + "empty group id": { + req: &group.MsgUpdateGroupMembers{ + GroupId: 0, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member2, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "no new members": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{}, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "invalid member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + {}, + }, + }, + expErr: true, + expErrMsg: "empty address string is not allowed", + }, + "invalid member metadata too long": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + { + Address: member2, + Weight: "2", + Metadata: strings.Repeat("a", 256), + }, + }, + }, + expErr: true, + expErrMsg: "group member metadata: limit exceeded", + }, + "add new member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member2, + Weight: "2", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "3", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + Member: &group.Member{ + Address: member2, + Weight: "2", + AddedAt: s.sdkCtx.BlockTime(), + }, + GroupId: groupID, + }, + { + Member: &group.Member{ + Address: member1, + Weight: "1", + AddedAt: s.blockTime, + }, + GroupId: groupID, + }, + }, + }, + "update member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "2", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "2", + AddedAt: s.blockTime, + }, + }, + }, + }, + "update member with same data": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "1", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + AddedAt: s.blockTime, + }, + }, + }, + }, + "replace member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + { + Address: member1, + Weight: "0", + }, + { + Address: member2, + Weight: "1", + }, + }, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member2, + Weight: "1", + AddedAt: s.sdkCtx.BlockTime(), + }, + }}, + }, + "remove existing member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "0", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "0", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{}, + }, + "remove unknown member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: addr4.String(), + Weight: "0", + }}, + }, + expErr: true, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + }, + }}, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: addr3.String(), + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "not group admin", + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + }, + }}, + }, + "with unknown groupID": { + req: &group.MsgUpdateGroupMembers{ + GroupId: 999, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "not found", + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + 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() + _, err := s.groupKeeper.UpdateGroupMembers(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroup, res.Info) + + // and members persisted + membersRes, err := s.groupKeeper.GroupMembers(sdkCtx, &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].Member.AddedAt, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(spec.expMembers[i].GroupId, loadedMembers[i].GroupId) + } + + events := sdkCtx.EventManager().ABCIEvents() + s.Require().Len(events, 1) // EventUpdateGroup + }) + } +} + +func (s *TestSuite) TestUpdateGroupAdmin() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + addr3 := addrs[2] + addr4 := addrs[3] + + members := []group.MemberRequest{{ + Address: addr1.String(), + Weight: "1", + }} + oldAdmin := addr2.String() + newAdmin := addr3.String() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: oldAdmin, + Members: members, + }) + s.Require().NoError(err) + groupID := groupRes.GroupId + specs := map[string]struct { + req *group.MsgUpdateGroupAdmin + expStored *group.GroupInfo + expErr bool + expErrMsg string + }{ + "with no groupID": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: 0, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "with identical admin and new admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: oldAdmin, + NewAdmin: oldAdmin, + }, + expErr: true, + expErrMsg: "new and old admin are the same", + }, + "with correct admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: newAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: addr4.String(), + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "not group admin", + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + "with unknown groupID": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: 999, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "not found", + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupAdmin(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.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, + }, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "3", + Version: 2, + CreatedAt: s.blockTime, + }, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupMetadata{ + GroupId: groupID, + Admin: addr3.String(), + }, + expErr: true, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + "with unknown groupid": { + req: &group.MsgUpdateGroupMetadata{ + GroupId: 999, + Admin: oldAdmin, + }, + expErr: true, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + _, err := s.groupKeeper.UpdateGroupMetadata(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) + s.Require().NoError(err) + s.Assert().Equal(spec.expStored, res.Info) + + events := sdkCtx.EventManager().ABCIEvents() + s.Require().Len(events, 1) // EventUpdateGroup + }) + } +} + +func (s *TestSuite) TestCreateGroupWithPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr3 := addrs[2] + addr5 := addrs[4] + addr6 := addrs[5] + + s.setNextAccount() + + members := []group.MemberRequest{{ + Address: addr5.String(), + Weight: "1", + }, { + Address: addr6.String(), + Weight: "2", + }} + + specs := map[string]struct { + req *group.MsgCreateGroupWithPolicy + policy group.DecisionPolicy + malleate func() + expErr bool + expErrMsg string + }{ + "all good": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "group policy as admin is true": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: true, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "group metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + GroupMetadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "group metadata: limit exceeded", + }, + "group policy metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + GroupPolicyMetadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "group policy metadata: limit exceeded", + }, + "member metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "1", + Metadata: strings.Repeat("a", 256), + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "member metadata: limit exceeded", + }, + "zero member weight": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "0", + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid member address": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: "invalid", + Weight: "1", + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "decoding bech32 failed", + }, + "decision policy threshold > total group weight": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + expErr: false, + }, + } + + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + s.setNextAccount() + err := spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() + res, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + id := res.GroupId + groupPolicyAddr := res.GroupPolicyAddress + + // then all data persisted in group + loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id}) + s.Require().NoError(err) + s.Assert().Equal(spec.req.GroupMetadata, loadedGroupRes.Info.Metadata) + s.Assert().Equal(id, loadedGroupRes.Info.Id) + if spec.req.GroupPolicyAsAdmin { + s.Assert().NotEqual(spec.req.Admin, loadedGroupRes.Info.Admin) + s.Assert().Equal(groupPolicyAddr, loadedGroupRes.Info.Admin) + } else { + s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin) + } + + // and members are stored as well + membersRes, err := s.groupKeeper.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(blockTime, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(id, loadedMembers[i].GroupId) + } + + // then all data persisted in group policy + groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: groupPolicyAddr}) + s.Require().NoError(err) + + groupPolicy := groupPolicyRes.Info + s.Assert().Equal(groupPolicyAddr, groupPolicy.Address) + s.Assert().Equal(id, groupPolicy.GroupId) + s.Assert().Equal(spec.req.GroupPolicyMetadata, groupPolicy.Metadata) + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) + if spec.req.GroupPolicyAsAdmin { + s.Assert().NotEqual(spec.req.Admin, groupPolicy.Admin) + s.Assert().Equal(groupPolicyAddr, groupPolicy.Admin) + } else { + s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) + } + }) + } +} + +func (s *TestSuite) TestCreateGroupPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr4 := addrs[3] + + s.setNextAccount() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: nil, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + specs := map[string]struct { + req *group.MsgCreateGroupPolicy + policy group.DecisionPolicy + expErr bool + expErrMsg string + }{ + "all good": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "all good with percentage decision policy": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "0.5", + time.Second, + 0, + ), + }, + "decision policy threshold > total group weight": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + }, + "group id does not exists": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: 9999, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "not found", + }, + "admin not group admin": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr4.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "not group admin", + }, + "metadata too long": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + Metadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "limit exceeded", + }, + "percentage decision policy with negative value": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "-0.5", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "percentage decision policy with value greater than 1": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "2", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "percentage must be > 0 and <= 1", + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + err := spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + s.setNextAccount() + + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + addr := res.Address + + // then all data persisted + groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: addr}) + s.Require().NoError(err) + + groupPolicy := groupPolicyRes.Info + s.Assert().Equal(addr, groupPolicy.Address) + s.Assert().Equal(myGroupID, groupPolicy.GroupId) + s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) + s.Assert().Equal(spec.req.Metadata, groupPolicy.Metadata) + s.Assert().Equal(uint64(1), groupPolicy.Version) + percentageDecisionPolicy, ok := spec.policy.(*group.PercentageDecisionPolicy) + if ok { + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(percentageDecisionPolicy, dp) + } else { + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) + } + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyAdmin() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + addr5 := addrs[4] + + admin, newAdmin := addr1, addr2 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + req *group.MsgUpdateGroupPolicyAdmin + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: newAdmin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + err := spec.expGroupPolicy.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyAdmin(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: groupPolicyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr5 := addrs[4] + + admin := addr1 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + preRun func(admin sdk.AccAddress) (policyAddr string, groupId uint64) + req *group.MsgUpdateGroupPolicyDecisionPolicy + policy group.DecisionPolicy + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: policy, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + }, + policy: policy, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "invalid percentage decision policy with negative value": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "-0.5", + time.Duration(1)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid percentage decision policy with value greater than 1": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "2", + time.Duration(1)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "percentage must be > 0 and <= 1", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewThresholdDecisionPolicy( + "2", + time.Duration(2)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + "correct data with percentage decision policy": { + preRun: func(admin sdk.AccAddress) (string, uint64) { + s.setNextAccount() + return s.createGroupAndGroupPolicy(admin, nil, policy) + }, + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "0.5", + time.Duration(2)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + DecisionPolicy: nil, + Version: 2, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + policyAddr := groupPolicyAddr + err := spec.expGroupPolicy.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + if spec.preRun != nil { + policyAddr1, groupID := spec.preRun(admin) + policyAddr = policyAddr1 + + // update the expected info with new group policy details + spec.expGroupPolicy.Address = policyAddr1 + spec.expGroupPolicy.GroupId = groupID + + // update req with new group policy addr + spec.req.GroupPolicyAddress = policyAddr1 + } + + err = spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyDecisionPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: policyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyMetadata() { + addrs := s.addrs + addr1 := addrs[0] + addr5 := addrs[4] + + admin := addr1 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + req *group.MsgUpdateGroupPolicyMetadata + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "with metadata too long": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + Metadata: strings.Repeat("a", 1001), + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "group policy metadata: limit exceeded", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + err := spec.expGroupPolicy.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyMetadata(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: groupPolicyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + + // check events + var hasUpdateGroupPolicyEvent bool + events := s.ctx.(sdk.Context).EventManager().ABCIEvents() + for _, event := range events { + event, err := sdk.ParseTypedEvent(event) + s.Require().NoError(err) + + if e, ok := event.(*group.EventUpdateGroupPolicy); ok { + s.Require().Equal(e.Address, groupPolicyAddr) + hasUpdateGroupPolicyEvent = true + break + } + } + + s.Require().True(hasUpdateGroupPolicyEvent) + }) + } +} + +func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() { + addrs := s.addrs + addr2 := addrs[1] + + admin := addr2 + + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: admin.String(), + Members: nil, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + policies := []group.DecisionPolicy{ + group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + group.NewPercentageDecisionPolicy( + "0.5", + time.Second, + 0, + ), + } + + count := 3 + expectAccs := make([]*group.GroupPolicyInfo, count) + for i := range expectAccs { + req := &group.MsgCreateGroupPolicy{ + Admin: admin.String(), + GroupId: myGroupID, + } + err := req.SetDecisionPolicy(policies[i]) + s.Require().NoError(err) + + s.setNextAccount() + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, req) + s.Require().NoError(err) + + expectAcc := &group.GroupPolicyInfo{ + Address: res.Address, + Admin: admin.String(), + GroupId: myGroupID, + Version: uint64(1), + CreatedAt: s.blockTime, + } + 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 policy by group + policiesByGroupRes, err := s.groupKeeper.GroupPoliciesByGroup(s.ctx, &group.QueryGroupPoliciesByGroupRequest{ + GroupId: myGroupID, + }) + s.Require().NoError(err) + policyAccs := policiesByGroupRes.GroupPolicies + s.Require().Equal(len(policyAccs), count) + // we reorder policyAccs by address to be able to compare them + sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) + for i := range policyAccs { + s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) + s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) + s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) + s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) + s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) + s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) + dp1, err := policyAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + dp2, err := expectAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(dp1, dp2) + } + + // query group policy by admin + policiesByAdminRes, err := s.groupKeeper.GroupPoliciesByAdmin(s.ctx, &group.QueryGroupPoliciesByAdminRequest{ + Admin: admin.String(), + }) + s.Require().NoError(err) + policyAccs = policiesByAdminRes.GroupPolicies + s.Require().Equal(len(policyAccs), count) + // we reorder policyAccs by address to be able to compare them + sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) + for i := range policyAccs { + s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) + s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) + s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) + s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) + s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) + s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) + dp1, err := policyAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + dp2, err := expectAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(dp1, dp2) + } +} + +func (s *TestSuite) TestSubmitProposal() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] // Has weight 2 + addr4 := addrs[3] + addr5 := addrs[4] // Has weight 1 + + myGroupID := s.groupID + accountAddr := s.groupPolicyAddr + + // Create a new group policy to test TRY_EXEC + policyReq := &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + } + noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy( + "2", + time.Second, + 0, // no MinExecutionPeriod to test TRY_EXEC + ) + err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy) + s.Require().NoError(err) + s.setNextAccount() + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + + noMinExecPeriodPolicyAddr, err := s.accountKeeper.AddressCodec().StringToBytes(res.Address) + s.Require().NoError(err) + + // Create a new group policy with super high threshold + bigThresholdPolicy := group.NewThresholdDecisionPolicy( + "100", + time.Second, + minExecutionPeriod, + ) + s.setNextAccount() + err = policyReq.SetDecisionPolicy(bigThresholdPolicy) + s.Require().NoError(err) + bigThresholdRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + bigThresholdAddr := bigThresholdRes.Address + + msgSend := &banktypes.MsgSend{ + FromAddress: res.Address, + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + defaultProposal := group.Proposal{ + GroupPolicyAddress: accountAddr.String(), + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + } + specs := map[string]struct { + req *group.MsgSubmitProposal + msgs []sdk.Msg + expProposal group.Proposal + expErr bool + expErrMsg string + postRun func(sdkCtx sdk.Context) + preRun func(msg []sdk.Msg) + }{ + "all good with minimal fields set": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + }, + expProposal: defaultProposal, + postRun: func(sdkCtx sdk.Context) {}, + }, + "all good with good msg payload": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: 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) {}, + }, + "title != metadata.title": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + Metadata: "{\"title\":\"title\",\"summary\":\"description\"}", + Title: "title2", + Summary: "description", + }, + expErr: true, + expErrMsg: "metadata title 'title' must equal proposal title 'title2'", + postRun: func(sdkCtx sdk.Context) {}, + }, + "summary != metadata.summary": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + Metadata: "{\"title\":\"title\",\"summary\":\"description of proposal\"}", + Title: "title", + Summary: "description", + }, + expErr: true, + expErrMsg: "metadata summary 'description of proposal' must equal proposal summary 'description'", + postRun: func(sdkCtx sdk.Context) {}, + }, + "metadata too long": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "limit exceeded", + postRun: func(sdkCtx sdk.Context) {}, + }, + "group policy required": { + req: &group.MsgSubmitProposal{ + Proposers: []string{addr2.String()}, + }, + expErr: true, + expErrMsg: "empty address string is not allowed", + postRun: func(sdkCtx sdk.Context) {}, + }, + "existing group policy required": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: addr1.String(), + Proposers: []string{addr2.String()}, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "decision policy threshold > total group weight": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: bigThresholdAddr, + Proposers: []string{addr2.String()}, + }, + expErr: false, + expProposal: group.Proposal{ + GroupPolicyAddress: bigThresholdAddr, + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.DefaultTallyResult(), + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + postRun: func(sdkCtx sdk.Context) {}, + }, + "only group members can create a proposal": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr4.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "all proposers must be in group": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String(), addr4.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "admin that is not a group member can not create proposal": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr1.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "reject msgs that are not authz by group policy": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + }, + msgs: []sdk.Msg{&testdata.TestMsg{Signers: []string{addr1.String()}}}, + expErr: true, + expErrMsg: "msg does not have group policy authorization", + postRun: func(sdkCtx sdk.Context) {}, + }, + "with try exec": { + preRun: func(msgs []sdk.Msg) { + for i := 0; i < len(msgs); i++ { + s.bankKeeper.EXPECT().Send(gomock.Any(), msgs[i]).Return(nil, nil) + } + }, + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: res.Address, + Proposers: []string{addr2.String()}, + Exec: group.Exec_EXEC_TRY, + }, + msgs: []sdk.Msg{msgSend}, + expProposal: group.Proposal{ + GroupPolicyAddress: res.Address, + Status: group.PROPOSAL_STATUS_ACCEPTED, + FinalTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + postRun: func(sdkCtx sdk.Context) { + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr) + s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) + toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) + s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) + events := sdkCtx.EventManager().ABCIEvents() + s.Require().True(eventTypeFound(events, EventTallyResult)) + }, + }, + "with try exec, not enough yes votes for proposal to pass": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: res.Address, + Proposers: []string{addr5.String()}, + Exec: group.Exec_EXEC_TRY, + }, + msgs: []sdk.Msg{msgSend}, + expProposal: group.Proposal{ + GroupPolicyAddress: res.Address, + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.TallyResult{ + YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + 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) + + if spec.preRun != nil { + spec.preRun(spec.msgs) + } + + res, err := s.groupKeeper.SubmitProposal(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + id := res.ProposalId + + if !(spec.expProposal.ExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + // then all data persisted + proposalRes, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: id}) + s.Require().NoError(err) + proposal := proposalRes.Proposal + + s.Assert().Equal(spec.expProposal.GroupPolicyAddress, proposal.GroupPolicyAddress) + s.Assert().Equal(spec.req.Metadata, proposal.Metadata) + s.Assert().Equal(spec.req.Proposers, proposal.Proposers) + s.Assert().Equal(s.blockTime, proposal.SubmitTime) + s.Assert().Equal(uint64(1), proposal.GroupVersion) + s.Assert().Equal(uint64(1), proposal.GroupPolicyVersion) + s.Assert().Equal(spec.expProposal.Status, proposal.Status) + s.Assert().Equal(spec.expProposal.FinalTallyResult, proposal.FinalTallyResult) + s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult) + s.Assert().Equal(s.blockTime.Add(time.Second), proposal.VotingPeriodEnd) + + msgs, err := proposal.GetMsgs() + s.Assert().NoError(err) + if spec.msgs == nil { // then empty list is ok + s.Assert().Len(msgs, 0) + } else { + s.Assert().Equal(spec.msgs, msgs) + } + } + + spec.postRun(s.sdkCtx) + }) + } +} + +func (s *TestSuite) TestWithdrawProposal() { + addrs := s.addrs + addr2 := addrs[1] + addr5 := addrs[4] + + msgSend := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + + proposers := []string{addr2.String()} + proposalID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + + specs := map[string]struct { + preRun func(sdkCtx sdk.Context) uint64 + proposalID uint64 + admin string + expErrMsg string + postRun func(sdkCtx sdk.Context) + }{ + "wrong admin": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + admin: addr5.String(), + expErrMsg: "unauthorized", + postRun: func(sdkCtx sdk.Context) {}, + }, + "wrong proposal id": { + preRun: func(sdkCtx sdk.Context) uint64 { + return 1111 + }, + admin: proposers[0], + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "happy case with proposer": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + proposalID: proposalID, + admin: proposers[0], + postRun: func(sdkCtx sdk.Context) {}, + }, + "already closed proposal": { + preRun: func(sdkCtx sdk.Context) uint64 { + pID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ + ProposalId: pID, + Address: proposers[0], + }) + s.Require().NoError(err) + return pID + }, + proposalID: proposalID, + admin: proposers[0], + expErrMsg: "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN", + postRun: func(sdkCtx sdk.Context) {}, + }, + "happy case with group admin address": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + proposalID: proposalID, + admin: proposers[0], + postRun: func(sdkCtx sdk.Context) { + resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + vpe := resp.Proposal.VotingPeriodEnd + timeDiff := vpe.Sub(s.sdkCtx.BlockTime()) + ctxVPE := sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(timeDiff).Add(time.Second * 1)) + s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctxVPE)) + events := ctxVPE.EventManager().ABCIEvents() + + s.Require().True(eventTypeFound(events, EventTallyResult)) + }, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + pID := spec.preRun(s.sdkCtx) + + _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ + ProposalId: pID, + Address: spec.admin, + }) + + if spec.expErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + + s.Require().NoError(err) + resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: pID}) + s.Require().NoError(err) + s.Require().Equal(resp.GetProposal().Status, group.PROPOSAL_STATUS_WITHDRAWN) + }) + 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.MemberRequest{ + {Address: addr4.String(), Weight: "1"}, + {Address: addr3.String(), Weight: "2"}, + } + + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + policy := group.NewThresholdDecisionPolicy( + "2", + time.Duration(2), + 0, + ) + policyReq := &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + } + err = policyReq.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.setNextAccount() + policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + accountAddr := policyRes.Address + // module account will be created and returned + groupPolicy, err := s.accountKeeper.AddressCodec().StringToBytes(accountAddr) + s.Require().NoError(err) + s.Require().NotNil(groupPolicy) + + s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)}).Return(nil).AnyTimes() + s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) + + req := &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr, + Proposers: []string{addr4.String()}, + Messages: nil, + } + msg := &banktypes.MsgSend{ + FromAddress: accountAddr, + ToAddress: addr5.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + err = req.SetMsgs([]sdk.Msg{msg}) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, req) + s.Require().NoError(err) + myProposalID := proposalRes.ProposalId + + // proposals by group policy + proposalsRes, err := s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{ + Address: accountAddr, + }) + s.Require().NoError(err) + proposals := proposalsRes.Proposals + s.Require().Equal(len(proposals), 1) + s.Assert().Equal(req.GroupPolicyAddress, proposals[0].GroupPolicyAddress) + s.Assert().Equal(req.Metadata, proposals[0].Metadata) + s.Assert().Equal(req.Proposers, proposals[0].Proposers) + s.Assert().Equal(s.blockTime, proposals[0].SubmitTime) + s.Assert().Equal(uint64(1), proposals[0].GroupVersion) + s.Assert().Equal(uint64(1), proposals[0].GroupPolicyVersion) + s.Assert().Equal(group.PROPOSAL_STATUS_SUBMITTED, proposals[0].Status) + s.Assert().Equal(group.DefaultTallyResult(), proposals[0].FinalTallyResult) + + specs := map[string]struct { + srcCtx sdk.Context + expTallyResult group.TallyResult // expected after tallying + isFinal bool // is the tally result final? + req *group.MsgVote + doBefore func(ctx context.Context) + postRun func(sdkCtx sdk.Context) + expProposalStatus group.ProposalStatus // expected after tallying + expExecutorResult group.ProposalExecutorResult // expected after tallying + expErr bool + expErrMsg string + }{ + "vote yes": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }, + expTallyResult: group.TallyResult{ + YesCount: "1", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "with try exec": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + Exec: group.Exec_EXEC_TRY, + }, + expTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + isFinal: true, + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + doBefore: func(ctx context.Context) { + s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) + }, + postRun: func(sdkCtx sdk.Context) { + s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), groupPolicy).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) + s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), addr5).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, groupPolicy) + s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) + toBalances := s.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(), + Option: group.VOTE_OPTION_YES, + Exec: group.Exec_EXEC_TRY, + }, + expTallyResult: group.TallyResult{ + YesCount: "1", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote no": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "1", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote abstain": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_ABSTAIN, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "1", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote veto": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO_WITH_VETO, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "1", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "apply decision policy early": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + }, + expTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "reject new votes when final decision is made already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }, + doBefore: func(ctx context.Context) { + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_NO_WITH_VETO, + Exec: 1, // Execute the proposal so that its status is final + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "proposal not open for voting", + postRun: func(sdkCtx sdk.Context) {}, + }, + "metadata too long": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "metadata: limit exceeded", + postRun: func(sdkCtx sdk.Context) {}, + }, + "existing proposal required": { + req: &group.MsgVote{ + ProposalId: 999, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "load proposal: not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "empty vote option": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + }, + expErr: true, + expErrMsg: "vote option: value is empty", + postRun: func(sdkCtx sdk.Context) {}, + }, + "invalid vote option": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: 5, + }, + expErr: true, + expErrMsg: "ote option: invalid value", + postRun: func(sdkCtx sdk.Context) {}, + }, + "voter must be in group": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr2.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "admin that is not a group member can not vote": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr1.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "on voting period end": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + srcCtx: s.sdkCtx.WithBlockTime(s.blockTime.Add(time.Second)), + expErr: true, + expErrMsg: "voting period has ended already: expired", + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote closed already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + doBefore: func(ctx context.Context) { + s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) + + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + Exec: 1, // Execute to close the proposal. + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "load proposal: not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "voted already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + doBefore: func(ctx context.Context) { + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "store vote: unique constraint violation", + 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() + if spec.doBefore != nil { + spec.doBefore(sdkCtx) + } + _, err := s.groupKeeper.Vote(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + // vote is stored and all data persisted + res, err := s.groupKeeper.VoteByProposalVoter(sdkCtx, &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.Option, loaded.Option) + s.Assert().Equal(spec.req.Metadata, loaded.Metadata) + s.Assert().Equal(s.blockTime, loaded.SubmitTime) + + // query votes by proposal + votesByProposalRes, err := s.groupKeeper.VotesByProposal(sdkCtx, &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.Option, vote.Option) + s.Assert().Equal(spec.req.Metadata, vote.Metadata) + s.Assert().Equal(s.blockTime, vote.SubmitTime) + + // query votes by voter + voter := spec.req.Voter + votesByVoterRes, err := s.groupKeeper.VotesByVoter(sdkCtx, &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.Option, votesByVoter[0].Option) + s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata) + s.Assert().Equal(s.blockTime, votesByVoter[0].SubmitTime) + + proposalRes, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ + ProposalId: spec.req.ProposalId, + }) + s.Require().NoError(err) + + proposal := proposalRes.Proposal + if spec.isFinal { + s.Assert().Equal(spec.expTallyResult, proposal.FinalTallyResult) + s.Assert().Equal(spec.expProposalStatus, proposal.Status) + s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult) + } else { + s.Assert().Equal(group.DefaultTallyResult(), proposal.FinalTallyResult) // Make sure proposal isn't mutated. + + // do a round of tallying + tallyResult, err := s.groupKeeper.Tally(sdkCtx, *proposal, myGroupID) + s.Require().NoError(err) + + s.Assert().Equal(spec.expTallyResult, tallyResult) + } + } + + spec.postRun(sdkCtx) + }) + } + + s.T().Log("test tally result should not take into account the member who left the group") + members = []group.MemberRequest{ + {Address: addr2.String(), Weight: "3"}, + {Address: addr3.String(), Weight: "2"}, + {Address: addr4.String(), Weight: "1"}, + } + reqCreate := &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupMetadata: "metadata", + } + + policy = group.NewThresholdDecisionPolicy( + "4", + time.Duration(10), + 0, + ) + s.Require().NoError(reqCreate.SetDecisionPolicy(policy)) + s.setNextAccount() + + result, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, reqCreate) + s.Require().NoError(err) + s.Require().NotNil(result) + + policyAddr := result.GroupPolicyAddress + groupID := result.GroupId + reqProposal := &group.MsgSubmitProposal{ + GroupPolicyAddress: policyAddr, + Proposers: []string{addr4.String()}, + } + s.Require().NoError(reqProposal.SetMsgs([]sdk.Msg{&banktypes.MsgSend{ + FromAddress: policyAddr, + ToAddress: addr5.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + }})) + + resSubmitProposal, err := s.groupKeeper.SubmitProposal(s.ctx, reqProposal) + s.Require().NoError(err) + s.Require().NotNil(resSubmitProposal) + proposalID := resSubmitProposal.ProposalId + + for _, voter := range []string{addr4.String(), addr3.String(), addr2.String()} { + _, err := s.groupKeeper.Vote(s.ctx, + &group.MsgVote{ProposalId: proposalID, Voter: voter, Option: group.VOTE_OPTION_YES}, + ) + s.Require().NoError(err) + } + + qProposals, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ + ProposalId: proposalID, + }) + s.Require().NoError(err) + + tallyResult, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) + s.Require().NoError(err) + + _, err = s.groupKeeper.LeaveGroup(s.ctx, &group.MsgLeaveGroup{Address: addr4.String(), GroupId: groupID}) + s.Require().NoError(err) + + tallyResult1, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) + s.Require().NoError(err) + s.Require().NotEqual(tallyResult.String(), tallyResult1.String()) +} + +func (s *TestSuite) TestExecProposal() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.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 + expErrMsg string + expProposalStatus group.ProposalStatus + expExecutorResult group.ProposalExecutorResult + expBalance bool + expFromBalances sdk.Coin + expToBalances sdk.Coin + postRun func(sdkCtx sdk.Context) + }{ + "proposal executed when accepted": { + setupProposal: func(ctx context.Context) uint64 { + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9900), + expToBalances: sdk.NewInt64Coin("test", 100), + postRun: func(sdkCtx sdk.Context) { + events := sdkCtx.EventManager().ABCIEvents() + s.Require().True(eventTypeFound(events, EventTallyResult)) + }, + }, + "proposal with multiple messages executed when accepted": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1, msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9800), + expToBalances: sdk.NewInt64Coin("test", 200), + postRun: func(sdkCtx sdk.Context) { + events := sdkCtx.EventManager().ABCIEvents() + s.Require().True(eventTypeFound(events, EventTallyResult)) + }, + }, + "proposal not executed when rejected": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) { + events := sdkCtx.EventManager().ABCIEvents() + s.Require().False(eventTypeFound(events, EventTallyResult)) + }, + }, + "open proposal must not fail": { + setupProposal: func(ctx context.Context) uint64 { + return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) { + events := sdkCtx.EventManager().ABCIEvents() + s.Require().False(eventTypeFound(events, EventTallyResult)) + }, + }, + "invalid proposal id": { + setupProposal: func(ctx context.Context) uint64 { + return 0 + }, + expErr: true, + expErrMsg: "proposal id: value is empty", + }, + "existing proposal required": { + setupProposal: func(ctx context.Context) uint64 { + return 9999 + }, + expErr: true, + expErrMsg: "load proposal: not found", + }, + "Decision policy also applied on exactly voting period end": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(time.Second), // Voting period is 1s + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "Decision policy also applied after voting period end": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "exec proposal before MinExecutionPeriod should fail": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed + postRun: func(sdkCtx sdk.Context) {}, + }, + "exec proposal at exactly MinExecutionPeriod should pass": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + postRun: func(sdkCtx sdk.Context) { + events := sdkCtx.EventManager().ABCIEvents() + s.Require().True(eventTypeFound(events, EventTallyResult)) + }, + }, + "prevent double execution when successful": { + setupProposal: func(ctx context.Context) uint64 { + myProposalID := submitProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.VOTE_OPTION_YES) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + + // Wait after min execution period end before Exec + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.Require().NoError(err) + return myProposalID + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expErr: true, // since proposal is pruned after a successful MsgExec + expErrMsg: "load proposal: not found", + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9900), + expToBalances: sdk.NewInt64Coin("test", 100), + postRun: func(sdkCtx sdk.Context) {}, + }, + "rollback all msg updates on failure": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1, msgSend2} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, + postRun: func(sdkCtx sdk.Context) {}, + }, + "executable when failed before": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend2} + myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + + // Wait after min execution period end before Exec + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) + + s.Require().NoError(err) + s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) + + return myProposalID + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + postRun: func(sdkCtx sdk.Context) {}, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + proposalID := spec.setupProposal(sdkCtx) + + if !spec.srcBlockTime.IsZero() { + sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) + } + + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + + // and proposal is updated + res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + proposal := res.Proposal + + exp := group.ProposalStatus_name[int32(spec.expProposalStatus)] + got := group.ProposalStatus_name[int32(proposal.Status)] + s.Assert().Equal(exp, got) + + exp = group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] + got = group.ProposalExecutorResult_name[int32(proposal.ExecutorResult)] + s.Assert().Equal(exp, got) + } + + if spec.expBalance { + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, s.groupPolicyAddr).Return(sdk.Coins{spec.expFromBalances}) + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.Coins{spec.expToBalances}) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, s.groupPolicyAddr) + s.Require().Contains(fromBalances, spec.expFromBalances) + toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) + s.Require().Contains(toBalances, spec.expToBalances) + } + spec.postRun(sdkCtx) + }) + + } +} + +func (s *TestSuite) TestExecPrunedProposalsAndVotes() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + + proposers := []string{addr2.String()} + specs := map[string]struct { + srcBlockTime time.Time + setupProposal func(ctx context.Context) uint64 + expErr bool + expErrMsg string + expExecutorResult group.ProposalExecutorResult + }{ + "proposal pruned after executor result success": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 101)}, + } + msgs := []sdk.Msg{msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + "proposal with multiple messages pruned when executed with result success": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 102)}, + } + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) + + msgs := []sdk.Msg{msgSend1, msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + "proposal not pruned when not executed and rejected": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 103)}, + } + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "open proposal is not pruned which must not fail ": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 104)}, + } + return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal not pruned with group modified before tally": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 105)}, + } + myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + + // then modify group + _, err := s.groupKeeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{ + Admin: addr1.String(), + GroupId: s.groupID, + }) + s.Require().NoError(err) + return myProposalID + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal not pruned with group policy modified before tally": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 106)}, + } + + myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + _, err := s.groupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{ + Admin: addr1.String(), + GroupPolicyAddress: s.groupPolicyAddr.String(), + }) + s.Require().NoError(err) + return myProposalID + }, + expErr: true, // since proposal status will be `aborted` when group policy is modified + expErrMsg: "not possible to exec with proposal status", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal exists when rollback all msg updates on failure": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 107)}, + } + + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 10002)}, + } + + msgs := []sdk.Msg{msgSend1, msgSend2} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, fmt.Errorf("error")) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, + }, + "pruned when proposal is executable when failed before": { + setupProposal: func(ctx context.Context) uint64 { + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 10003)}, + } + + msgs := []sdk.Msg{msgSend2} + + myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + + // Wait for min execution period end + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) + + s.Require().NoError(err) + return myProposalID + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + proposalID := spec.setupProposal(sdkCtx) + + if !spec.srcBlockTime.IsZero() { + sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) + } + + // Wait for min execution period end + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS { + // Make sure proposal is deleted from state + _, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().Contains(err.Error(), spec.expErrMsg) + res, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + s.Require().Empty(res.GetVotes()) + events := sdkCtx.EventManager().ABCIEvents() + s.Require().True(eventTypeFound(events, EventTallyResult)) + + } else { + // Check that proposal and votes exists + res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + _, err = s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id}) + s.Require().NoError(err) + s.Require().Equal("", spec.expErrMsg) + + exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] + got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)] + s.Assert().Equal(exp, got) + } + }) + } +} + +func (s *TestSuite) TestLeaveGroup() { + addrs := simtestutil.CreateIncrementalAccounts(7) + + admin1 := addrs[0] + member1 := addrs[1] + member2 := addrs[2] + member3 := addrs[3] + member4 := addrs[4] + admin2 := addrs[5] + admin3 := addrs[6] + + members := []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + { + Address: member2.String(), + Weight: "2", + Metadata: "metadata", + }, + { + Address: member3.String(), + Weight: "3", + Metadata: "metadata", + }, + } + policy := group.NewThresholdDecisionPolicy( + "3", + time.Hour, + time.Hour, + ) + s.setNextAccount() + _, groupID1 := s.createGroupAndGroupPolicy(admin1, members, policy) + + members = []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + } + + s.setNextAccount() + _, groupID2 := s.createGroupAndGroupPolicy(admin2, members, nil) + + members = []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + { + Address: member2.String(), + Weight: "2", + Metadata: "metadata", + }, + } + policy = &group.PercentageDecisionPolicy{ + Percentage: "0.5", + Windows: &group.DecisionPolicyWindows{VotingPeriod: time.Hour}, + } + + s.setNextAccount() + + _, groupID3 := s.createGroupAndGroupPolicy(admin3, members, policy) + testCases := []struct { + name string + req *group.MsgLeaveGroup + expErr bool + expErrMsg string + expMembersSize int + memberWeight math.Dec + }{ + { + "group not found", + &group.MsgLeaveGroup{ + GroupId: 100000, + Address: member1.String(), + }, + true, + "group: not found", + 0, + math.NewDecFromInt64(0), + }, + { + "member address invalid", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: "invalid", + }, + true, + "decoding bech32 failed", + 0, + math.NewDecFromInt64(0), + }, + { + "member not part of group", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member4.String(), + }, + true, + "not part of group", + 0, + math.NewDecFromInt64(0), + }, + { + "valid testcase: decision policy is not present (and group total weight can be 0)", + &group.MsgLeaveGroup{ + GroupId: groupID2, + Address: member1.String(), + }, + false, + "", + 0, + math.NewDecFromInt64(1), + }, + { + "valid testcase: threshold decision policy", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member3.String(), + }, + false, + "", + 2, + math.NewDecFromInt64(3), + }, + { + "valid request: can leave group policy threshold more than group weight", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member2.String(), + }, + false, + "", + 1, + math.NewDecFromInt64(2), + }, + { + "valid request: can leave group (percentage decision policy)", + &group.MsgLeaveGroup{ + GroupId: groupID3, + Address: member2.String(), + }, + false, + "", + 1, + math.NewDecFromInt64(2), + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + var groupWeight1 math.Dec + if !tc.expErr { + groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) + s.Require().NoError(err) + groupWeight1, err = math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) + s.Require().NoError(err) + } + + res, err := s.groupKeeper.LeaveGroup(s.ctx, tc.req) + if tc.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expErrMsg) + } else { + s.Require().NoError(err) + s.Require().NotNil(res) + res, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{ + GroupId: tc.req.GroupId, + }) + s.Require().NoError(err) + s.Require().Len(res.Members, tc.expMembersSize) + + groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) + s.Require().NoError(err) + groupWeight2, err := math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) + s.Require().NoError(err) + + rWeight, err := groupWeight1.Sub(tc.memberWeight) + s.Require().NoError(err) + s.Require().Equal(rWeight.Cmp(groupWeight2), 0) + } + }) + } +} + +func (s *TestSuite) TestExecProposalsWhenMemberLeavesOrIsUpdated() { + proposers := []string{s.addrs[1].String()} + + specs := map[string]struct { + votes []group.VoteOption + members []group.MemberRequest + setupProposal func(ctx context.Context, groupPolicyAddr string) uint64 + malleate func(ctx context.Context, k keeper.Keeper, groupPolicyAddr string, groupID uint64) error + expErrMsg string + }{ + "member leaves while all others vote yes: proposal accepted": { + members: []group.MemberRequest{ + {Address: s.addrs[4].String(), Weight: "1"}, + {Address: s.addrs[1].String(), Weight: "2"}, + {Address: s.addrs[3].String(), Weight: "1"}, + {Address: s.addrs[5].String(), Weight: "2"}, + {Address: s.addrs[2].String(), Weight: "2"}, + }, + votes: []group.VoteOption{ + group.VOTE_OPTION_YES, group.VOTE_OPTION_YES, + group.VOTE_OPTION_YES, group.VOTE_OPTION_YES, + group.VOTE_OPTION_YES, + }, + setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: groupPolicyAddr, + ToAddress: s.addrs[1].String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + + // the proposal will pass and be executed + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(1) + + msgs := []sdk.Msg{msgSend1} + proposalReq := &group.MsgSubmitProposal{ + GroupPolicyAddress: groupPolicyAddr, + Proposers: proposers, + } + err := proposalReq.SetMsgs(msgs) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq) + s.Require().NoError(err) + + return proposalRes.ProposalId + }, + malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error { + _, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[5].String()}) + return err + }, + }, + "member leaves while all others vote yes and no: proposal rejected": { + members: []group.MemberRequest{ + {Address: s.addrs[4].String(), Weight: "2"}, + {Address: s.addrs[1].String(), Weight: "2"}, + {Address: s.addrs[3].String(), Weight: "2"}, + {Address: s.addrs[2].String(), Weight: "2"}, + }, + votes: []group.VoteOption{ + group.VOTE_OPTION_NO, group.VOTE_OPTION_NO, + group.VOTE_OPTION_YES, group.VOTE_OPTION_YES, + }, + setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: groupPolicyAddr, + ToAddress: s.addrs[1].String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + msgs := []sdk.Msg{msgSend1, msgSend1} + + proposalReq := &group.MsgSubmitProposal{ + GroupPolicyAddress: groupPolicyAddr, + Proposers: proposers, + } + err := proposalReq.SetMsgs(msgs) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq) + s.Require().NoError(err) + + return proposalRes.ProposalId + }, + malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error { + _, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[3].String()}) + return err + }, + }, + "member that leaves does affect the threshold policy outcome": { + members: []group.MemberRequest{ + {Address: s.addrs[3].String(), Weight: "6"}, + {Address: s.addrs[1].String(), Weight: "1"}, + {Address: s.addrs[5].String(), Weight: "1"}, + {Address: s.addrs[2].String(), Weight: "1"}, + }, + votes: []group.VoteOption{ + group.VOTE_OPTION_YES, group.VOTE_OPTION_NO, + group.VOTE_OPTION_YES, group.VOTE_OPTION_YES, + }, + setupProposal: func(ctx context.Context, addr string) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: addr, + ToAddress: s.addrs[1].String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + msgs := []sdk.Msg{msgSend1, msgSend1} + + proposalReq := &group.MsgSubmitProposal{ + GroupPolicyAddress: addr, + Proposers: proposers, + } + err := proposalReq.SetMsgs(msgs) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq) + s.Require().NoError(err) + + return proposalRes.ProposalId + }, + malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error { + _, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[3].String()}) + return err + }, + }, + "update group policy voids the proposal": { + members: []group.MemberRequest{ + {Address: s.addrs[3].String(), Weight: "2"}, + {Address: s.addrs[2].String(), Weight: "2"}, + {Address: s.addrs[1].String(), Weight: "2"}, + {Address: s.addrs[4].String(), Weight: "2"}, + }, + votes: []group.VoteOption{ + group.VOTE_OPTION_YES, group.VOTE_OPTION_NO, + group.VOTE_OPTION_YES, group.VOTE_OPTION_NO, + }, + setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: groupPolicyAddr, + ToAddress: s.addrs[1].String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + msgs := []sdk.Msg{msgSend1, msgSend1} + proposalReq := &group.MsgSubmitProposal{ + GroupPolicyAddress: groupPolicyAddr, + Proposers: proposers, + } + err := proposalReq.SetMsgs(msgs) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq) + s.Require().NoError(err) + + return proposalRes.ProposalId + }, + malleate: func(ctx context.Context, k keeper.Keeper, groupPolicyAddr string, groupID uint64) error { + newGroupPolicy := &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: s.addrs[0].String(), + GroupPolicyAddress: groupPolicyAddr, + } + newGroupPolicy.SetDecisionPolicy(group.NewThresholdDecisionPolicy("10", time.Second, minExecutionPeriod)) + + _, err := k.UpdateGroupPolicyDecisionPolicy(ctx, newGroupPolicy) + return err + }, + expErrMsg: "PROPOSAL_STATUS_ABORTED", + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + + s.setNextAccount() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: s.addrs[0].String(), + Members: spec.members, + }) + s.Require().NoError(err) + groupID := groupRes.GroupId + + policy := group.NewThresholdDecisionPolicy("4", time.Second, minExecutionPeriod) + policyReq := &group.MsgCreateGroupPolicy{ + Admin: s.addrs[0].String(), + GroupId: groupID, + } + err = policyReq.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.setNextAccount() + + s.groupKeeper.GetGroupSequence(s.sdkCtx) + policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + + // Setup and submit proposal + proposalID := spec.setupProposal(sdkCtx, policyRes.Address) + + // vote on the proposals + for i, vote := range spec.votes { + _, err := s.groupKeeper.Vote(sdkCtx, &group.MsgVote{ + ProposalId: proposalID, + Voter: spec.members[i].Address, + Option: vote, + }) + s.Require().NoError(err) + } + + err = spec.malleate(sdkCtx, s.groupKeeper, policyRes.Address, groupID) + s.Require().NoError(err) + + // travel in time + sdkCtx = sdkCtx.WithBlockTime(s.blockTime.Add(minExecutionPeriod + 1)) + _, err = s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: s.addrs[1].String(), ProposalId: proposalID}) + if spec.expErrMsg != "" { + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + }) + } +} + +func eventTypeFound(events []abci.Event, eventType string) bool { + eventTypeFound := false + for _, e := range events { + if e.Type == eventType { + eventTypeFound = true + break + } + } + return eventTypeFound +} diff --git a/x/group/types.pb.go b/x/group/types.pb.go index 882d8e42893c..5c41140622a3 100644 --- a/x/group/types.pb.go +++ b/x/group/types.pb.go @@ -642,6 +642,11 @@ type GroupPolicyInfo struct { // admin is the account address of the group admin. Admin string `protobuf:"bytes,3,opt,name=admin,proto3" json:"admin,omitempty"` // metadata is any arbitrary metadata attached to the group policy. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#decision-policy-1 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) Metadata string `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` // version is used to track changes to a group's GroupPolicyInfo structure that // would create a different result on a running proposal. @@ -695,6 +700,11 @@ type Proposal struct { // group_policy_address is the account address of group policy. GroupPolicyAddress string `protobuf:"bytes,2,opt,name=group_policy_address,json=groupPolicyAddress,proto3" json:"group_policy_address,omitempty"` // metadata is any arbitrary metadata attached to the proposal. +<<<<<<< HEAD +======= + // the recommended format of the metadata is to be found here: + // https://docs.cosmos.network/v0.47/modules/group#proposal-4 +>>>>>>> be2003e58 (feat(group): add group event tally result (#16191)) Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` // proposers are the account addresses of the proposers. Proposers []string `protobuf:"bytes,4,rep,name=proposers,proto3" json:"proposers,omitempty"`