Synopsis
Event
s are objects that contain information about the execution of the application. They are mainly used by service providers like block explorers and wallet to track the execution of various messages and index transactions.
Pre-requisite Readings
Events
Events are implemented in the Cosmos SDK as an alias of the ABCIEvent
type and
take the form of: {eventType}.{attributeKey}={attributeValue}
.
Copy
Ask AI
// Event allows application developers to attach additional information to
// ResponseBeginBlock, ResponseEndBlock, ResponseCheckTx and ResponseDeliverTx.
// Later, transactions may be queried using these events.
message Event {
string type = 1;
repeated EventAttribute attributes = 2 [
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "attributes,omitempty"
];
}
- A
type
to categorize the Event at a high-level; for example, the Cosmos SDK uses the"message"
type to filter Events byMsg
s. - A list of
attributes
are key-value pairs that give more information about the categorized Event. For example, for the"message"
type, we can filter Events by key-value pairs usingmessage.action={some_action}
,message.module={some_module}
ormessage.sender={some_sender}
. - A
msg_index
to identify which messages relate to the same transaction
To parse the attribute values as strings, make sure to add
'
(single quotes)
around each attribute value.event.proto
file, on a per-module basis and are read as proto.Message
.
Legacy Events are defined on a per-module basis in the module’s /types/events.go
file.
They are triggered from the module’s Protobuf Msg
service
by using the EventManager
.
In addition, each module documents its events under in the Events
sections of its specs (x/{moduleName}
/README.md
).
Lastly, Events are returned to the underlying consensus engine in the response of the following ABCI messages:
Examples
The following examples show how to query Events using the Cosmos SDK.Event | Description |
---|---|
tx.height=23 | Query all transactions at height 23 |
message.action='/cosmos.bank.v1beta1.Msg/Send' | Query all transactions containing a x/bank Send Service Msg . Note the ' s around the value. |
message.module='bank' | Query all transactions containing messages from the x/bank module. Note the ' s around the value. |
create_validator.validator='cosmosval1...' | x/staking-specific Event, see x/staking SPEC. |
EventManager
In Cosmos SDK applications, Events are managed by an abstraction called theEventManager
.
Internally, the EventManager
tracks a list of Events for the entire execution flow of FinalizeBlock
(i.e. transaction execution, BeginBlock
, EndBlock
).
Copy
Ask AI
package types
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cosmos/gogoproto/jsonpb"
proto "github.com/cosmos/gogoproto/proto"
"github.com/cosmos/cosmos-sdk/codec"
)
type EventManagerI interface {
Events()
Events
ABCIEvents() []abci.Event
EmitTypedEvent(tev proto.Message)
error
EmitTypedEvents(tevs ...proto.Message)
error
EmitEvent(event Event)
EmitEvents(events Events)
}
/ ----------------------------------------------------------------------------
/ Event Manager
/ ----------------------------------------------------------------------------
var _ EventManagerI = (*EventManager)(nil)
/ EventManager implements a simple wrapper around a slice of Event objects that
/ can be emitted from.
type EventManager struct {
events Events
}
func NewEventManager() *EventManager {
return &EventManager{
EmptyEvents()
}
}
func (em *EventManager)
Events()
Events {
return em.events
}
/ EmitEvent stores a single Event object.
/ Deprecated: Use EmitTypedEvent
func (em *EventManager)
EmitEvent(event Event) {
em.events = em.events.AppendEvent(event)
}
/ EmitEvents stores a series of Event objects.
/ Deprecated: Use EmitTypedEvents
func (em *EventManager)
EmitEvents(events Events) {
em.events = em.events.AppendEvents(events)
}
/ ABCIEvents returns all stored Event objects as abci.Event objects.
func (em EventManager)
ABCIEvents() []abci.Event {
return em.events.ToABCIEvents()
}
/ EmitTypedEvent takes typed event and emits converting it into Event
func (em *EventManager)
EmitTypedEvent(tev proto.Message)
error {
event, err := TypedEventToEvent(tev)
if err != nil {
return err
}
em.EmitEvent(event)
return nil
}
/ EmitTypedEvents takes series of typed events and emit
func (em *EventManager)
EmitTypedEvents(tevs ...proto.Message)
error {
events := make(Events, len(tevs))
for i, tev := range tevs {
res, err := TypedEventToEvent(tev)
if err != nil {
return err
}
events[i] = res
}
em.EmitEvents(events)
return nil
}
/ TypedEventToEvent takes typed event and converts to Event object
func TypedEventToEvent(tev proto.Message) (Event, error) {
evtType := proto.MessageName(tev)
evtJSON, err := codec.ProtoMarshalJSON(tev, nil)
if err != nil {
return Event{
}, err
}
var attrMap map[string]json.RawMessage
err = json.Unmarshal(evtJSON, &attrMap)
if err != nil {
return Event{
}, err
}
/ sort the keys to ensure the order is always the same
keys := maps.Keys(attrMap)
slices.Sort(keys)
attrs := make([]abci.EventAttribute, 0, len(attrMap))
for _, k := range keys {
v := attrMap[k]
attrs = append(attrs, abci.EventAttribute{
Key: k,
Value: string(v),
})
}
return Event{
Type: evtType,
Attributes: attrs,
}, nil
}
/ ParseTypedEvent converts abci.Event back to a typed event.
func ParseTypedEvent(event abci.Event) (proto.Message, error) {
concreteGoType := proto.MessageType(event.Type)
if concreteGoType == nil {
return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type)
}
var value reflect.Value
if concreteGoType.Kind() == reflect.Ptr {
value = reflect.New(concreteGoType.Elem())
}
else {
value = reflect.Zero(concreteGoType)
}
protoMsg, ok := value.Interface().(proto.Message)
if !ok {
return nil, fmt.Errorf("%q does not implement proto.Message", event.Type)
}
attrMap := make(map[string]json.RawMessage)
for _, attr := range event.Attributes {
attrMap[attr.Key] = json.RawMessage(attr.Value)
}
attrBytes, err := json.Marshal(attrMap)
if err != nil {
return nil, err
}
unmarshaler := jsonpb.Unmarshaler{
AllowUnknownFields: true
}
if err := unmarshaler.Unmarshal(strings.NewReader(string(attrBytes)), protoMsg); err != nil {
return nil, err
}
return protoMsg, nil
}
/ ----------------------------------------------------------------------------
/ Events
/ ----------------------------------------------------------------------------
type (
/ Event is a type alias for an ABCI Event
Event abci.Event
/ Events defines a slice of Event objects
Events []Event
)
/ NewEvent creates a new Event object with a given type and slice of one or more
/ attributes.
func NewEvent(ty string, attrs ...Attribute)
Event {
e := Event{
Type: ty
}
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
/ NewAttribute returns a new key/value Attribute object.
func NewAttribute(k, v string)
Attribute {
return Attribute{
k, v
}
}
/ EmptyEvents returns an empty slice of events.
func EmptyEvents()
Events {
return make(Events, 0)
}
func (a Attribute)
String()
string {
return fmt.Sprintf("%s: %s", a.Key, a.Value)
}
/ ToKVPair converts an Attribute object into a CometBFT key/value pair.
func (a Attribute)
ToKVPair()
abci.EventAttribute {
return abci.EventAttribute{
Key: a.Key,
Value: a.Value
}
}
/ AppendAttributes adds one or more attributes to an Event.
func (e Event)
AppendAttributes(attrs ...Attribute)
Event {
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
/ GetAttribute returns an attribute for a given key present in an event.
/ If the key is not found, the boolean value will be false.
func (e Event)
GetAttribute(key string) (Attribute, bool) {
for _, attr := range e.Attributes {
if attr.Key == key {
return Attribute{
Key: attr.Key,
Value: attr.Value
}, true
}
}
return Attribute{
}, false
}
/ AppendEvent adds an Event to a slice of events.
func (e Events)
AppendEvent(event Event)
Events {
return append(e, event)
}
/ AppendEvents adds a slice of Event objects to an exist slice of Event objects.
func (e Events)
AppendEvents(events Events)
Events {
return append(e, events...)
}
/ ToABCIEvents converts a slice of Event objects to a slice of abci.Event
/ objects.
func (e Events)
ToABCIEvents() []abci.Event {
res := make([]abci.Event, len(e))
for i, ev := range e {
res[i] = abci.Event{
Type: ev.Type,
Attributes: ev.Attributes
}
}
return res
}
/ GetAttributes returns all attributes matching a given key present in events.
/ If the key is not found, the boolean value will be false.
func (e Events)
GetAttributes(key string) ([]Attribute, bool) {
attrs := make([]Attribute, 0)
for _, event := range e {
if attr, found := event.GetAttribute(key); found {
attrs = append(attrs, attr)
}
}
return attrs, len(attrs) > 0
}
/ Common event types and attribute keys
const (
EventTypeTx = "tx"
AttributeKeyAccountSequence = "acc_seq"
AttributeKeySignature = "signature"
AttributeKeyFee = "fee"
AttributeKeyFeePayer = "fee_payer"
EventTypeMessage = "message"
AttributeKeyAction = "action"
AttributeKeyModule = "module"
AttributeKeySender = "sender"
AttributeKeyAmount = "amount"
)
type (
/ StringAttributes defines a slice of StringEvents objects.
StringEvents []StringEvent
)
func (se StringEvents)
String()
string {
var sb strings.Builder
for _, e := range se {
fmt.Fprintf(&sb, "\t\t- %s\n", e.Type)
for _, attr := range e.Attributes {
fmt.Fprintf(&sb, "\t\t\t- %s\n", attr)
}
}
return strings.TrimRight(sb.String(), "\n")
}
/ StringifyEvent converts an Event object to a StringEvent object.
func StringifyEvent(e abci.Event)
StringEvent {
res := StringEvent{
Type: e.Type
}
for _, attr := range e.Attributes {
res.Attributes = append(
res.Attributes,
Attribute{
Key: attr.Key,
Value: attr.Value
},
)
}
return res
}
/ StringifyEvents converts a slice of Event objects into a slice of StringEvent
/ objects.
func StringifyEvents(events []abci.Event)
StringEvents {
res := make(StringEvents, 0, len(events))
for _, e := range events {
res = append(res, StringifyEvent(e))
}
return res
}
/ MarkEventsToIndex returns the set of ABCI events, where each event's attribute
/ has it's index value marked based on the provided set of events to index.
func MarkEventsToIndex(events []abci.Event, indexSet map[string]struct{
}) []abci.Event {
indexAll := len(indexSet) == 0
updatedEvents := make([]abci.Event, len(events))
for i, e := range events {
updatedEvent := abci.Event{
Type: e.Type,
Attributes: make([]abci.EventAttribute, len(e.Attributes)),
}
for j, attr := range e.Attributes {
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
updatedAttr := abci.EventAttribute{
Key: attr.Key,
Value: attr.Value,
Index: index || indexAll,
}
updatedEvent.Attributes[j] = updatedAttr
}
updatedEvents[i] = updatedEvent
}
return updatedEvents
}
EventManager
comes with a set of useful methods to manage Events. The method
that is used most by module and application developers is EmitTypedEvent
or EmitEvent
that tracks
an Event in the EventManager
.
Copy
Ask AI
package types
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cosmos/gogoproto/jsonpb"
proto "github.com/cosmos/gogoproto/proto"
"github.com/cosmos/cosmos-sdk/codec"
)
type EventManagerI interface {
Events()
Events
ABCIEvents() []abci.Event
EmitTypedEvent(tev proto.Message)
error
EmitTypedEvents(tevs ...proto.Message)
error
EmitEvent(event Event)
EmitEvents(events Events)
}
/ ----------------------------------------------------------------------------
/ Event Manager
/ ----------------------------------------------------------------------------
var _ EventManagerI = (*EventManager)(nil)
/ EventManager implements a simple wrapper around a slice of Event objects that
/ can be emitted from.
type EventManager struct {
events Events
}
func NewEventManager() *EventManager {
return &EventManager{
EmptyEvents()
}
}
func (em *EventManager)
Events()
Events {
return em.events
}
/ EmitEvent stores a single Event object.
/ Deprecated: Use EmitTypedEvent
func (em *EventManager)
EmitEvent(event Event) {
em.events = em.events.AppendEvent(event)
}
/ EmitEvents stores a series of Event objects.
/ Deprecated: Use EmitTypedEvents
func (em *EventManager)
EmitEvents(events Events) {
em.events = em.events.AppendEvents(events)
}
/ ABCIEvents returns all stored Event objects as abci.Event objects.
func (em EventManager)
ABCIEvents() []abci.Event {
return em.events.ToABCIEvents()
}
/ EmitTypedEvent takes typed event and emits converting it into Event
func (em *EventManager)
EmitTypedEvent(tev proto.Message)
error {
event, err := TypedEventToEvent(tev)
if err != nil {
return err
}
em.EmitEvent(event)
return nil
}
/ EmitTypedEvents takes series of typed events and emit
func (em *EventManager)
EmitTypedEvents(tevs ...proto.Message)
error {
events := make(Events, len(tevs))
for i, tev := range tevs {
res, err := TypedEventToEvent(tev)
if err != nil {
return err
}
events[i] = res
}
em.EmitEvents(events)
return nil
}
/ TypedEventToEvent takes typed event and converts to Event object
func TypedEventToEvent(tev proto.Message) (Event, error) {
evtType := proto.MessageName(tev)
evtJSON, err := codec.ProtoMarshalJSON(tev, nil)
if err != nil {
return Event{
}, err
}
var attrMap map[string]json.RawMessage
err = json.Unmarshal(evtJSON, &attrMap)
if err != nil {
return Event{
}, err
}
/ sort the keys to ensure the order is always the same
keys := maps.Keys(attrMap)
slices.Sort(keys)
attrs := make([]abci.EventAttribute, 0, len(attrMap))
for _, k := range keys {
v := attrMap[k]
attrs = append(attrs, abci.EventAttribute{
Key: k,
Value: string(v),
})
}
return Event{
Type: evtType,
Attributes: attrs,
}, nil
}
/ ParseTypedEvent converts abci.Event back to a typed event.
func ParseTypedEvent(event abci.Event) (proto.Message, error) {
concreteGoType := proto.MessageType(event.Type)
if concreteGoType == nil {
return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type)
}
var value reflect.Value
if concreteGoType.Kind() == reflect.Ptr {
value = reflect.New(concreteGoType.Elem())
}
else {
value = reflect.Zero(concreteGoType)
}
protoMsg, ok := value.Interface().(proto.Message)
if !ok {
return nil, fmt.Errorf("%q does not implement proto.Message", event.Type)
}
attrMap := make(map[string]json.RawMessage)
for _, attr := range event.Attributes {
attrMap[attr.Key] = json.RawMessage(attr.Value)
}
attrBytes, err := json.Marshal(attrMap)
if err != nil {
return nil, err
}
unmarshaler := jsonpb.Unmarshaler{
AllowUnknownFields: true
}
if err := unmarshaler.Unmarshal(strings.NewReader(string(attrBytes)), protoMsg); err != nil {
return nil, err
}
return protoMsg, nil
}
/ ----------------------------------------------------------------------------
/ Events
/ ----------------------------------------------------------------------------
type (
/ Event is a type alias for an ABCI Event
Event abci.Event
/ Events defines a slice of Event objects
Events []Event
)
/ NewEvent creates a new Event object with a given type and slice of one or more
/ attributes.
func NewEvent(ty string, attrs ...Attribute)
Event {
e := Event{
Type: ty
}
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
/ NewAttribute returns a new key/value Attribute object.
func NewAttribute(k, v string)
Attribute {
return Attribute{
k, v
}
}
/ EmptyEvents returns an empty slice of events.
func EmptyEvents()
Events {
return make(Events, 0)
}
func (a Attribute)
String()
string {
return fmt.Sprintf("%s: %s", a.Key, a.Value)
}
/ ToKVPair converts an Attribute object into a CometBFT key/value pair.
func (a Attribute)
ToKVPair()
abci.EventAttribute {
return abci.EventAttribute{
Key: a.Key,
Value: a.Value
}
}
/ AppendAttributes adds one or more attributes to an Event.
func (e Event)
AppendAttributes(attrs ...Attribute)
Event {
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
/ GetAttribute returns an attribute for a given key present in an event.
/ If the key is not found, the boolean value will be false.
func (e Event)
GetAttribute(key string) (Attribute, bool) {
for _, attr := range e.Attributes {
if attr.Key == key {
return Attribute{
Key: attr.Key,
Value: attr.Value
}, true
}
}
return Attribute{
}, false
}
/ AppendEvent adds an Event to a slice of events.
func (e Events)
AppendEvent(event Event)
Events {
return append(e, event)
}
/ AppendEvents adds a slice of Event objects to an exist slice of Event objects.
func (e Events)
AppendEvents(events Events)
Events {
return append(e, events...)
}
/ ToABCIEvents converts a slice of Event objects to a slice of abci.Event
/ objects.
func (e Events)
ToABCIEvents() []abci.Event {
res := make([]abci.Event, len(e))
for i, ev := range e {
res[i] = abci.Event{
Type: ev.Type,
Attributes: ev.Attributes
}
}
return res
}
/ GetAttributes returns all attributes matching a given key present in events.
/ If the key is not found, the boolean value will be false.
func (e Events)
GetAttributes(key string) ([]Attribute, bool) {
attrs := make([]Attribute, 0)
for _, event := range e {
if attr, found := event.GetAttribute(key); found {
attrs = append(attrs, attr)
}
}
return attrs, len(attrs) > 0
}
/ Common event types and attribute keys
const (
EventTypeTx = "tx"
AttributeKeyAccountSequence = "acc_seq"
AttributeKeySignature = "signature"
AttributeKeyFee = "fee"
AttributeKeyFeePayer = "fee_payer"
EventTypeMessage = "message"
AttributeKeyAction = "action"
AttributeKeyModule = "module"
AttributeKeySender = "sender"
AttributeKeyAmount = "amount"
)
type (
/ StringAttributes defines a slice of StringEvents objects.
StringEvents []StringEvent
)
func (se StringEvents)
String()
string {
var sb strings.Builder
for _, e := range se {
fmt.Fprintf(&sb, "\t\t- %s\n", e.Type)
for _, attr := range e.Attributes {
fmt.Fprintf(&sb, "\t\t\t- %s\n", attr)
}
}
return strings.TrimRight(sb.String(), "\n")
}
/ StringifyEvent converts an Event object to a StringEvent object.
func StringifyEvent(e abci.Event)
StringEvent {
res := StringEvent{
Type: e.Type
}
for _, attr := range e.Attributes {
res.Attributes = append(
res.Attributes,
Attribute{
Key: attr.Key,
Value: attr.Value
},
)
}
return res
}
/ StringifyEvents converts a slice of Event objects into a slice of StringEvent
/ objects.
func StringifyEvents(events []abci.Event)
StringEvents {
res := make(StringEvents, 0, len(events))
for _, e := range events {
res = append(res, StringifyEvent(e))
}
return res
}
/ MarkEventsToIndex returns the set of ABCI events, where each event's attribute
/ has it's index value marked based on the provided set of events to index.
func MarkEventsToIndex(events []abci.Event, indexSet map[string]struct{
}) []abci.Event {
indexAll := len(indexSet) == 0
updatedEvents := make([]abci.Event, len(events))
for i, e := range events {
updatedEvent := abci.Event{
Type: e.Type,
Attributes: make([]abci.EventAttribute, len(e.Attributes)),
}
for j, attr := range e.Attributes {
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
updatedAttr := abci.EventAttribute{
Key: attr.Key,
Value: attr.Value,
Index: index || indexAll,
}
updatedEvent.Attributes[j] = updatedAttr
}
updatedEvents[i] = updatedEvent
}
return updatedEvents
}
EventManager#EmitTypedEvent
or EventManager#EmitEvent
in each message
Handler
and in each BeginBlock
/EndBlock
handler. The EventManager
is accessed via
the Context
, where Event should be already registered, and emitted like this:
Typed events:
Copy
Ask AI
package keeper
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"strings"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/cosmos/cosmos-sdk/x/group/errors"
"github.com/cosmos/cosmos-sdk/x/group/internal/math"
"github.com/cosmos/cosmos-sdk/x/group/internal/orm"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
var _ group.MsgServer = Keeper{
}
/ TODO: Revisit this once we have proper gas fee framework.
/ Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072
const gasCostPerIteration = uint64(20)
func (k Keeper)
CreateGroup(goCtx context.Context, msg *group.MsgCreateGroup) (*group.MsgCreateGroupResponse, error) {
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.Admin); err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid admin address: %s", msg.Admin)
}
if err := k.validateMembers(msg.Members); err != nil {
return nil, errorsmod.Wrap(err, "members")
}
if err := k.assertMetadataLength(msg.Metadata, "group metadata"); err != nil {
return nil, err
}
totalWeight := math.NewDecFromInt64(0)
for _, m := range msg.Members {
if err := k.assertMetadataLength(m.Metadata, "member metadata"); err != nil {
return nil, err
}
/ Members of a group must have a positive weight.
/ NOTE: group member with zero weight are only allowed when updating group members.
/ If the member has a zero weight, it will be removed from the group.
weight, err := math.NewPositiveDecFromString(m.Weight)
if err != nil {
return nil, err
}
/ Adding up members weights to compute group total weight.
totalWeight, err = totalWeight.Add(weight)
if err != nil {
return nil, err
}
}
/ Create a new group in the groupTable.
ctx := sdk.UnwrapSDKContext(goCtx)
groupInfo := &group.GroupInfo{
Id: k.groupTable.Sequence().PeekNextVal(ctx.KVStore(k.key)),
Admin: msg.Admin,
Metadata: msg.Metadata,
Version: 1,
TotalWeight: totalWeight.String(),
CreatedAt: ctx.BlockTime(),
}
groupID, err := k.groupTable.Create(ctx.KVStore(k.key), groupInfo)
if err != nil {
return nil, errorsmod.Wrap(err, "could not create group")
}
/ Create new group members in the groupMemberTable.
for i, m := range msg.Members {
err := k.groupMemberTable.Create(ctx.KVStore(k.key), &group.GroupMember{
GroupId: groupID,
Member: &group.Member{
Address: m.Address,
Weight: m.Weight,
Metadata: m.Metadata,
AddedAt: ctx.BlockTime(),
},
})
if err != nil {
return nil, errorsmod.Wrapf(err, "could not store member %d", i)
}
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventCreateGroup{
GroupId: groupID
}); err != nil {
return nil, err
}
return &group.MsgCreateGroupResponse{
GroupId: groupID
}, nil
}
func (k Keeper)
UpdateGroupMembers(goCtx context.Context, msg *group.MsgUpdateGroupMembers) (*group.MsgUpdateGroupMembersResponse, error) {
if msg.GroupId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "group id")
}
if len(msg.MemberUpdates) == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "member updates")
}
if err := k.validateMembers(msg.MemberUpdates); err != nil {
return nil, errorsmod.Wrap(err, "members")
}
ctx := sdk.UnwrapSDKContext(goCtx)
action := func(g *group.GroupInfo)
error {
totalWeight, err := math.NewNonNegativeDecFromString(g.TotalWeight)
if err != nil {
return errorsmod.Wrap(err, "group total weight")
}
for _, member := range msg.MemberUpdates {
if err := k.assertMetadataLength(member.Metadata, "group member metadata"); err != nil {
return err
}
groupMember := group.GroupMember{
GroupId: msg.GroupId,
Member: &group.Member{
Address: member.Address,
Weight: member.Weight,
Metadata: member.Metadata,
},
}
/ Checking if the group member is already part of the group
var found bool
var prevGroupMember group.GroupMember
switch err := k.groupMemberTable.GetOne(ctx.KVStore(k.key), orm.PrimaryKey(&groupMember), &prevGroupMember); {
case err == nil:
found = true
case sdkerrors.ErrNotFound.Is(err):
found = false
default:
return errorsmod.Wrap(err, "get group member")
}
newMemberWeight, err := math.NewNonNegativeDecFromString(groupMember.Member.Weight)
if err != nil {
return err
}
/ Handle delete for members with zero weight.
if newMemberWeight.IsZero() {
/ We can't delete a group member that doesn't already exist.
if !found {
return errorsmod.Wrap(sdkerrors.ErrNotFound, "unknown member")
}
previousMemberWeight, err := math.NewPositiveDecFromString(prevGroupMember.Member.Weight)
if err != nil {
return err
}
/ Subtract the weight of the group member to delete from the group total weight.
totalWeight, err = math.SubNonNegative(totalWeight, previousMemberWeight)
if err != nil {
return err
}
/ Delete group member in the groupMemberTable.
if err := k.groupMemberTable.Delete(ctx.KVStore(k.key), &groupMember); err != nil {
return errorsmod.Wrap(err, "delete member")
}
continue
}
/ If group member already exists, handle update
if found {
previousMemberWeight, err := math.NewPositiveDecFromString(prevGroupMember.Member.Weight)
if err != nil {
return err
}
/ Subtract previous weight from the group total weight.
totalWeight, err = math.SubNonNegative(totalWeight, previousMemberWeight)
if err != nil {
return err
}
/ Save updated group member in the groupMemberTable.
groupMember.Member.AddedAt = prevGroupMember.Member.AddedAt
if err := k.groupMemberTable.Update(ctx.KVStore(k.key), &groupMember); err != nil {
return errorsmod.Wrap(err, "add member")
}
}
else { / else handle create.
groupMember.Member.AddedAt = ctx.BlockTime()
if err := k.groupMemberTable.Create(ctx.KVStore(k.key), &groupMember); err != nil {
return errorsmod.Wrap(err, "add member")
}
}
/ In both cases (handle + update), we need to add the new member's weight to the group total weight.
totalWeight, err = totalWeight.Add(newMemberWeight)
if err != nil {
return err
}
}
/ Update group in the groupTable.
g.TotalWeight = totalWeight.String()
g.Version++
if err := k.validateDecisionPolicies(ctx, *g); err != nil {
return err
}
return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g)
}
if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "members updated"); err != nil {
return nil, err
}
return &group.MsgUpdateGroupMembersResponse{
}, nil
}
func (k Keeper)
UpdateGroupAdmin(goCtx context.Context, msg *group.MsgUpdateGroupAdmin) (*group.MsgUpdateGroupAdminResponse, error) {
if msg.GroupId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "group id")
}
if strings.EqualFold(msg.Admin, msg.NewAdmin) {
return nil, errorsmod.Wrap(errors.ErrInvalid, "new and old admin are the same")
}
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.Admin); err != nil {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "admin address")
}
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.NewAdmin); err != nil {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "new admin address")
}
ctx := sdk.UnwrapSDKContext(goCtx)
action := func(g *group.GroupInfo)
error {
g.Admin = msg.NewAdmin
g.Version++
return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g)
}
if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "admin updated"); err != nil {
return nil, err
}
return &group.MsgUpdateGroupAdminResponse{
}, nil
}
func (k Keeper)
UpdateGroupMetadata(goCtx context.Context, msg *group.MsgUpdateGroupMetadata) (*group.MsgUpdateGroupMetadataResponse, error) {
if msg.GroupId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "group id")
}
if err := k.assertMetadataLength(msg.Metadata, "group metadata"); err != nil {
return nil, err
}
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.Admin); err != nil {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "admin address")
}
ctx := sdk.UnwrapSDKContext(goCtx)
action := func(g *group.GroupInfo)
error {
g.Metadata = msg.Metadata
g.Version++
return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g)
}
if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "metadata updated"); err != nil {
return nil, err
}
return &group.MsgUpdateGroupMetadataResponse{
}, nil
}
func (k Keeper)
CreateGroupWithPolicy(ctx context.Context, msg *group.MsgCreateGroupWithPolicy) (*group.MsgCreateGroupWithPolicyResponse, error) {
/ NOTE: admin, and group message validation is performed in the CreateGroup method
groupRes, err := k.CreateGroup(ctx, &group.MsgCreateGroup{
Admin: msg.Admin,
Members: msg.Members,
Metadata: msg.GroupMetadata,
})
if err != nil {
return nil, errorsmod.Wrap(err, "group response")
}
groupID := groupRes.GroupId
/ NOTE: group policy message validation is performed in the CreateGroupPolicy method
groupPolicyRes, err := k.CreateGroupPolicy(ctx, &group.MsgCreateGroupPolicy{
Admin: msg.Admin,
GroupId: groupID,
Metadata: msg.GroupPolicyMetadata,
DecisionPolicy: msg.DecisionPolicy,
})
if err != nil {
return nil, errorsmod.Wrap(err, "group policy response")
}
if msg.GroupPolicyAsAdmin {
updateAdminReq := &group.MsgUpdateGroupAdmin{
GroupId: groupID,
Admin: msg.Admin,
NewAdmin: groupPolicyRes.Address,
}
_, err = k.UpdateGroupAdmin(ctx, updateAdminReq)
if err != nil {
return nil, err
}
updatePolicyAddressReq := &group.MsgUpdateGroupPolicyAdmin{
Admin: msg.Admin,
GroupPolicyAddress: groupPolicyRes.Address,
NewAdmin: groupPolicyRes.Address,
}
_, err = k.UpdateGroupPolicyAdmin(ctx, updatePolicyAddressReq)
if err != nil {
return nil, err
}
}
return &group.MsgCreateGroupWithPolicyResponse{
GroupId: groupID,
GroupPolicyAddress: groupPolicyRes.Address
}, nil
}
func (k Keeper)
CreateGroupPolicy(goCtx context.Context, msg *group.MsgCreateGroupPolicy) (*group.MsgCreateGroupPolicyResponse, error) {
if msg.GroupId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "group id")
}
if err := k.assertMetadataLength(msg.GetMetadata(), "group policy metadata"); err != nil {
return nil, err
}
policy, err := msg.GetDecisionPolicy()
if err != nil {
return nil, errorsmod.Wrap(err, "request decision policy")
}
if err := policy.ValidateBasic(); err != nil {
return nil, errorsmod.Wrap(err, "decision policy")
}
reqGroupAdmin, err := k.accKeeper.AddressCodec().StringToBytes(msg.GetAdmin())
if err != nil {
return nil, errorsmod.Wrap(err, "request admin")
}
ctx := sdk.UnwrapSDKContext(goCtx)
groupInfo, err := k.getGroupInfo(ctx, msg.GetGroupID())
if err != nil {
return nil, err
}
groupAdmin, err := k.accKeeper.AddressCodec().StringToBytes(groupInfo.Admin)
if err != nil {
return nil, errorsmod.Wrap(err, "group admin")
}
/ Only current group admin is authorized to create a group policy for this
if !bytes.Equal(groupAdmin, reqGroupAdmin) {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "not group admin")
}
if err := policy.Validate(groupInfo, k.config); err != nil {
return nil, err
}
/ Generate account address of group policy.
var accountAddr sdk.AccAddress
/ loop here in the rare case where a ADR-028-derived address creates a
/ collision with an existing address.
for {
nextAccVal := k.groupPolicySeq.NextVal(ctx.KVStore(k.key))
derivationKey := make([]byte, 8)
binary.BigEndian.PutUint64(derivationKey, nextAccVal)
ac, err := authtypes.NewModuleCredential(group.ModuleName, []byte{
GroupPolicyTablePrefix
}, derivationKey)
if err != nil {
return nil, err
}
accountAddr = sdk.AccAddress(ac.Address())
if k.accKeeper.GetAccount(ctx, accountAddr) != nil {
/ handle a rare collision, in which case we just go on to the
/ next sequence value and derive a new address.
continue
}
/ group policy accounts are unclaimable base accounts
account, err := authtypes.NewBaseAccountWithPubKey(ac)
if err != nil {
return nil, errorsmod.Wrap(err, "could not create group policy account")
}
acc := k.accKeeper.NewAccount(ctx, account)
k.accKeeper.SetAccount(ctx, acc)
break
}
groupPolicy, err := group.NewGroupPolicyInfo(
accountAddr,
msg.GetGroupID(),
reqGroupAdmin,
msg.GetMetadata(),
1,
policy,
ctx.BlockTime(),
)
if err != nil {
return nil, err
}
if err := k.groupPolicyTable.Create(ctx.KVStore(k.key), &groupPolicy); err != nil {
return nil, errorsmod.Wrap(err, "could not create group policy")
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventCreateGroupPolicy{
Address: accountAddr.String()
}); err != nil {
return nil, err
}
return &group.MsgCreateGroupPolicyResponse{
Address: accountAddr.String()
}, nil
}
func (k Keeper)
UpdateGroupPolicyAdmin(goCtx context.Context, msg *group.MsgUpdateGroupPolicyAdmin) (*group.MsgUpdateGroupPolicyAdminResponse, error) {
if strings.EqualFold(msg.Admin, msg.NewAdmin) {
return nil, errorsmod.Wrap(errors.ErrInvalid, "new and old admin are same")
}
ctx := sdk.UnwrapSDKContext(goCtx)
action := func(groupPolicy *group.GroupPolicyInfo)
error {
groupPolicy.Admin = msg.NewAdmin
groupPolicy.Version++
return k.groupPolicyTable.Update(ctx.KVStore(k.key), groupPolicy)
}
if err := k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy admin updated"); err != nil {
return nil, err
}
return &group.MsgUpdateGroupPolicyAdminResponse{
}, nil
}
func (k Keeper)
UpdateGroupPolicyDecisionPolicy(goCtx context.Context, msg *group.MsgUpdateGroupPolicyDecisionPolicy) (*group.MsgUpdateGroupPolicyDecisionPolicyResponse, error) {
policy, err := msg.GetDecisionPolicy()
if err != nil {
return nil, errorsmod.Wrap(err, "decision policy")
}
if err := policy.ValidateBasic(); err != nil {
return nil, errorsmod.Wrap(err, "decision policy")
}
ctx := sdk.UnwrapSDKContext(goCtx)
action := func(groupPolicy *group.GroupPolicyInfo)
error {
groupInfo, err := k.getGroupInfo(ctx, groupPolicy.GroupId)
if err != nil {
return err
}
err = policy.Validate(groupInfo, k.config)
if err != nil {
return err
}
err = groupPolicy.SetDecisionPolicy(policy)
if err != nil {
return err
}
groupPolicy.Version++
return k.groupPolicyTable.Update(ctx.KVStore(k.key), groupPolicy)
}
if err = k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy's decision policy updated"); err != nil {
return nil, err
}
return &group.MsgUpdateGroupPolicyDecisionPolicyResponse{
}, nil
}
func (k Keeper)
UpdateGroupPolicyMetadata(goCtx context.Context, msg *group.MsgUpdateGroupPolicyMetadata) (*group.MsgUpdateGroupPolicyMetadataResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
metadata := msg.GetMetadata()
action := func(groupPolicy *group.GroupPolicyInfo)
error {
groupPolicy.Metadata = metadata
groupPolicy.Version++
return k.groupPolicyTable.Update(ctx.KVStore(k.key), groupPolicy)
}
if err := k.assertMetadataLength(metadata, "group policy metadata"); err != nil {
return nil, err
}
err := k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy metadata updated")
if err != nil {
return nil, err
}
return &group.MsgUpdateGroupPolicyMetadataResponse{
}, nil
}
func (k Keeper)
SubmitProposal(goCtx context.Context, msg *group.MsgSubmitProposal) (*group.MsgSubmitProposalResponse, error) {
if len(msg.Proposers) == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "proposers")
}
if err := k.validateProposers(msg.Proposers); err != nil {
return nil, err
}
groupPolicyAddr, err := k.accKeeper.AddressCodec().StringToBytes(msg.GroupPolicyAddress)
if err != nil {
return nil, errorsmod.Wrap(err, "request account address of group policy")
}
if err := k.assertMetadataLength(msg.Title, "proposal Title"); err != nil {
return nil, err
}
if err := k.assertMetadataLength(msg.Summary, "proposal summary"); err != nil {
return nil, err
}
if err := k.assertMetadataLength(msg.Metadata, "metadata"); err != nil {
return nil, err
}
/ verify that if present, the metadata title and summary equals the proposal title and summary
if len(msg.Metadata) != 0 {
proposalMetadata := govtypes.ProposalMetadata{
}
if err := json.Unmarshal([]byte(msg.Metadata), &proposalMetadata); err == nil {
if proposalMetadata.Title != msg.Title {
return nil, fmt.Errorf("metadata title '%s' must equal proposal title '%s'", proposalMetadata.Title, msg.Title)
}
if proposalMetadata.Summary != msg.Summary {
return nil, fmt.Errorf("metadata summary '%s' must equal proposal summary '%s'", proposalMetadata.Summary, msg.Summary)
}
}
/ if we can't unmarshal the metadata, this means the client didn't use the recommended metadata format
/ nothing can be done here, and this is still a valid case, so we ignore the error
}
msgs, err := msg.GetMsgs()
if err != nil {
return nil, errorsmod.Wrap(err, "request msgs")
}
if err := validateMsgs(msgs); err != nil {
return nil, err
}
ctx := sdk.UnwrapSDKContext(goCtx)
policyAcc, err := k.getGroupPolicyInfo(ctx, msg.GroupPolicyAddress)
if err != nil {
return nil, errorsmod.Wrapf(err, "load group policy: %s", msg.GroupPolicyAddress)
}
groupInfo, err := k.getGroupInfo(ctx, policyAcc.GroupId)
if err != nil {
return nil, errorsmod.Wrap(err, "get group by groupId of group policy")
}
/ Only members of the group can submit a new proposal.
for _, proposer := range msg.Proposers {
if !k.groupMemberTable.Has(ctx.KVStore(k.key), orm.PrimaryKey(&group.GroupMember{
GroupId: groupInfo.Id,
Member: &group.Member{
Address: proposer
}})) {
return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "not in group: %s", proposer)
}
}
/ Check that if the messages require signers, they are all equal to the given account address of group policy.
if err := ensureMsgAuthZ(msgs, groupPolicyAddr, k.cdc); err != nil {
return nil, err
}
policy, err := policyAcc.GetDecisionPolicy()
if err != nil {
return nil, errorsmod.Wrap(err, "proposal group policy decision policy")
}
/ Prevent proposal that cannot succeed.
if err = policy.Validate(groupInfo, k.config); err != nil {
return nil, err
}
m := &group.Proposal{
Id: k.proposalTable.Sequence().PeekNextVal(ctx.KVStore(k.key)),
GroupPolicyAddress: msg.GroupPolicyAddress,
Metadata: msg.Metadata,
Proposers: msg.Proposers,
SubmitTime: ctx.BlockTime(),
GroupVersion: groupInfo.Version,
GroupPolicyVersion: policyAcc.Version,
Status: group.PROPOSAL_STATUS_SUBMITTED,
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
VotingPeriodEnd: ctx.BlockTime().Add(policy.GetVotingPeriod()), / The voting window begins as soon as the proposal is submitted.
FinalTallyResult: group.DefaultTallyResult(),
Title: msg.Title,
Summary: msg.Summary,
}
if err := m.SetMsgs(msgs); err != nil {
return nil, errorsmod.Wrap(err, "create proposal")
}
id, err := k.proposalTable.Create(ctx.KVStore(k.key), m)
if err != nil {
return nil, errorsmod.Wrap(err, "create proposal")
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventSubmitProposal{
ProposalId: id
}); err != nil {
return nil, err
}
/ Try to execute proposal immediately
if msg.Exec == group.Exec_EXEC_TRY {
/ Consider proposers as Yes votes
for _, proposer := range msg.Proposers {
ctx.GasMeter().ConsumeGas(gasCostPerIteration, "vote on proposal")
_, err = k.Vote(ctx, &group.MsgVote{
ProposalId: id,
Voter: proposer,
Option: group.VOTE_OPTION_YES,
})
if err != nil {
return &group.MsgSubmitProposalResponse{
ProposalId: id
}, errorsmod.Wrapf(err, "the proposal was created but failed on vote for voter %s", proposer)
}
}
/ Then try to execute the proposal
_, err = k.Exec(ctx, &group.MsgExec{
ProposalId: id,
/ We consider the first proposer as the MsgExecRequest signer
/ but that could be revisited (eg using the group policy)
Executor: msg.Proposers[0],
})
if err != nil {
return &group.MsgSubmitProposalResponse{
ProposalId: id
}, errorsmod.Wrap(err, "the proposal was created but failed on exec")
}
}
return &group.MsgSubmitProposalResponse{
ProposalId: id
}, nil
}
func (k Keeper)
WithdrawProposal(goCtx context.Context, msg *group.MsgWithdrawProposal) (*group.MsgWithdrawProposalResponse, error) {
if msg.ProposalId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id")
}
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.Address); err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid group policy admin / proposer address: %s", msg.Address)
}
ctx := sdk.UnwrapSDKContext(goCtx)
proposal, err := k.getProposal(ctx, msg.ProposalId)
if err != nil {
return nil, err
}
/ Ensure the proposal can be withdrawn.
if proposal.Status != group.PROPOSAL_STATUS_SUBMITTED {
return nil, errorsmod.Wrapf(errors.ErrInvalid, "cannot withdraw a proposal with the status of %s", proposal.Status.String())
}
var policyInfo group.GroupPolicyInfo
if policyInfo, err = k.getGroupPolicyInfo(ctx, proposal.GroupPolicyAddress); err != nil {
return nil, errorsmod.Wrap(err, "load group policy")
}
/ check address is the group policy admin he is in proposers list..
if msg.Address != policyInfo.Admin && !isProposer(proposal, msg.Address) {
return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "given address is neither group policy admin nor in proposers: %s", msg.Address)
}
proposal.Status = group.PROPOSAL_STATUS_WITHDRAWN
if err := k.proposalTable.Update(ctx.KVStore(k.key), msg.ProposalId, &proposal); err != nil {
return nil, err
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventWithdrawProposal{
ProposalId: msg.ProposalId
}); err != nil {
return nil, err
}
return &group.MsgWithdrawProposalResponse{
}, nil
}
func (k Keeper)
Vote(goCtx context.Context, msg *group.MsgVote) (*group.MsgVoteResponse, error) {
if msg.ProposalId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id")
}
/ verify vote options
if msg.Option == group.VOTE_OPTION_UNSPECIFIED {
return nil, errorsmod.Wrap(errors.ErrEmpty, "vote option")
}
if _, ok := group.VoteOption_name[int32(msg.Option)]; !ok {
return nil, errorsmod.Wrap(errors.ErrInvalid, "vote option")
}
if err := k.assertMetadataLength(msg.Metadata, "metadata"); err != nil {
return nil, err
}
if _, err := k.accKeeper.AddressCodec().StringToBytes(msg.Voter); err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid voter address: %s", msg.Voter)
}
ctx := sdk.UnwrapSDKContext(goCtx)
proposal, err := k.getProposal(ctx, msg.ProposalId)
if err != nil {
return nil, err
}
/ Ensure that we can still accept votes for this proposal.
if proposal.Status != group.PROPOSAL_STATUS_SUBMITTED {
return nil, errorsmod.Wrap(errors.ErrInvalid, "proposal not open for voting")
}
if ctx.BlockTime().After(proposal.VotingPeriodEnd) {
return nil, errorsmod.Wrap(errors.ErrExpired, "voting period has ended already")
}
policyInfo, err := k.getGroupPolicyInfo(ctx, proposal.GroupPolicyAddress)
if err != nil {
return nil, errorsmod.Wrap(err, "load group policy")
}
groupInfo, err := k.getGroupInfo(ctx, policyInfo.GroupId)
if err != nil {
return nil, err
}
/ Count and store votes.
voter := group.GroupMember{
GroupId: groupInfo.Id,
Member: &group.Member{
Address: msg.Voter
}}
if err := k.groupMemberTable.GetOne(ctx.KVStore(k.key), orm.PrimaryKey(&voter), &voter); err != nil {
return nil, errorsmod.Wrapf(err, "voter address: %s", msg.Voter)
}
newVote := group.Vote{
ProposalId: msg.ProposalId,
Voter: msg.Voter,
Option: msg.Option,
Metadata: msg.Metadata,
SubmitTime: ctx.BlockTime(),
}
/ The ORM will return an error if the vote already exists,
/ making sure than a voter hasn't already voted.
if err := k.voteTable.Create(ctx.KVStore(k.key), &newVote); err != nil {
return nil, errorsmod.Wrap(err, "store vote")
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventVote{
ProposalId: msg.ProposalId
}); err != nil {
return nil, err
}
/ Try to execute proposal immediately
if msg.Exec == group.Exec_EXEC_TRY {
_, err = k.Exec(ctx, &group.MsgExec{
ProposalId: msg.ProposalId,
Executor: msg.Voter
})
if err != nil {
return nil, err
}
}
return &group.MsgVoteResponse{
}, nil
}
/ doTallyAndUpdate performs a tally, and, if the tally result is final, then:
/ - updates the proposal's `Status` and `FinalTallyResult` fields,
/ - prune all the votes.
func (k Keeper)
doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, groupInfo group.GroupInfo, policyInfo group.GroupPolicyInfo)
error {
policy, err := policyInfo.GetDecisionPolicy()
if err != nil {
return err
}
tallyResult, err := k.Tally(ctx, *p, policyInfo.GroupId)
if err != nil {
return err
}
result, err := policy.Allow(tallyResult, groupInfo.TotalWeight)
if err != nil {
return errorsmod.Wrap(err, "policy allow")
}
/ If the result was final (i.e. enough votes to pass)
or if the voting
/ period ended, then we consider the proposal as final.
if isFinal := result.Final || ctx.BlockTime().After(p.VotingPeriodEnd); isFinal {
if err := k.pruneVotes(ctx, p.Id); err != nil {
return err
}
p.FinalTallyResult = tallyResult
if result.Allow {
p.Status = group.PROPOSAL_STATUS_ACCEPTED
}
else {
p.Status = group.PROPOSAL_STATUS_REJECTED
}
}
return nil
}
/ Exec executes the messages from a proposal.
func (k Keeper)
Exec(goCtx context.Context, msg *group.MsgExec) (*group.MsgExecResponse, error) {
if msg.ProposalId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id")
}
ctx := sdk.UnwrapSDKContext(goCtx)
proposal, err := k.getProposal(ctx, msg.ProposalId)
if err != nil {
return nil, err
}
if proposal.Status != group.PROPOSAL_STATUS_SUBMITTED && proposal.Status != group.PROPOSAL_STATUS_ACCEPTED {
return nil, errorsmod.Wrapf(errors.ErrInvalid, "not possible to exec with proposal status %s", proposal.Status.String())
}
policyInfo, err := k.getGroupPolicyInfo(ctx, proposal.GroupPolicyAddress)
if err != nil {
return nil, errorsmod.Wrap(err, "load group policy")
}
/ If proposal is still in SUBMITTED phase, it means that the voting period
/ didn't end yet, and tallying hasn't been done. In this case, we need to
/ tally first.
if proposal.Status == group.PROPOSAL_STATUS_SUBMITTED {
groupInfo, err := k.getGroupInfo(ctx, policyInfo.GroupId)
if err != nil {
return nil, errorsmod.Wrap(err, "load group")
}
if err = k.doTallyAndUpdate(ctx, &proposal, groupInfo, policyInfo); err != nil {
return nil, err
}
}
/ Execute proposal payload.
var logs string
if proposal.Status == group.PROPOSAL_STATUS_ACCEPTED && proposal.ExecutorResult != group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
/ Caching context so that we don't update the store in case of failure.
cacheCtx, flush := ctx.CacheContext()
addr, err := k.accKeeper.AddressCodec().StringToBytes(policyInfo.Address)
if err != nil {
return nil, err
}
decisionPolicy := policyInfo.DecisionPolicy.GetCachedValue().(group.DecisionPolicy)
if results, err := k.doExecuteMsgs(cacheCtx, k.router, proposal, addr, decisionPolicy); err != nil {
proposal.ExecutorResult = group.PROPOSAL_EXECUTOR_RESULT_FAILURE
logs = fmt.Sprintf("proposal execution failed on proposal %d, because of error %s", proposal.Id, err.Error())
k.Logger(ctx).Info("proposal execution failed", "cause", err, "proposalID", proposal.Id)
}
else {
proposal.ExecutorResult = group.PROPOSAL_EXECUTOR_RESULT_SUCCESS
flush()
for _, res := range results {
/ NOTE: The sdk msg handler creates a new EventManager, so events must be correctly propagated back to the current context
ctx.EventManager().EmitEvents(res.GetEvents())
}
}
}
/ Update proposal in proposalTable
/ If proposal has successfully run, delete it from state.
if proposal.ExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
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, proposal.Id, &proposal); err != nil {
return nil, err
}
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventExec{
ProposalId: proposal.Id,
Logs: logs,
Result: proposal.ExecutorResult,
}); err != nil {
return nil, err
}
return &group.MsgExecResponse{
Result: proposal.ExecutorResult,
}, nil
}
/ LeaveGroup implements the MsgServer/LeaveGroup method.
func (k Keeper)
LeaveGroup(goCtx context.Context, msg *group.MsgLeaveGroup) (*group.MsgLeaveGroupResponse, error) {
if msg.GroupId == 0 {
return nil, errorsmod.Wrap(errors.ErrEmpty, "group-id")
}
_, err := k.accKeeper.AddressCodec().StringToBytes(msg.Address)
if err != nil {
return nil, errorsmod.Wrap(err, "group member")
}
ctx := sdk.UnwrapSDKContext(goCtx)
groupInfo, err := k.getGroupInfo(ctx, msg.GroupId)
if err != nil {
return nil, errorsmod.Wrap(err, "group")
}
groupWeight, err := math.NewNonNegativeDecFromString(groupInfo.TotalWeight)
if err != nil {
return nil, err
}
gm, err := k.getGroupMember(ctx, &group.GroupMember{
GroupId: msg.GroupId,
Member: &group.Member{
Address: msg.Address
},
})
if err != nil {
return nil, err
}
memberWeight, err := math.NewPositiveDecFromString(gm.Member.Weight)
if err != nil {
return nil, err
}
updatedWeight, err := math.SubNonNegative(groupWeight, memberWeight)
if err != nil {
return nil, err
}
/ delete group member in the groupMemberTable.
if err := k.groupMemberTable.Delete(ctx.KVStore(k.key), gm); err != nil {
return nil, errorsmod.Wrap(err, "group member")
}
/ update group weight
groupInfo.TotalWeight = updatedWeight.String()
groupInfo.Version++
if err := k.validateDecisionPolicies(ctx, groupInfo); err != nil {
return nil, err
}
if err := k.groupTable.Update(ctx.KVStore(k.key), groupInfo.Id, &groupInfo); err != nil {
return nil, err
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventLeaveGroup{
GroupId: msg.GroupId,
Address: msg.Address,
}); err != nil {
return nil, err
}
return &group.MsgLeaveGroupResponse{
}, nil
}
func (k Keeper)
getGroupMember(ctx sdk.Context, member *group.GroupMember) (*group.GroupMember, error) {
var groupMember group.GroupMember
switch err := k.groupMemberTable.GetOne(ctx.KVStore(k.key),
orm.PrimaryKey(member), &groupMember); {
case err == nil:
break
case sdkerrors.ErrNotFound.Is(err):
return nil, sdkerrors.ErrNotFound.Wrapf("%s is not part of group %d", member.Member.Address, member.GroupId)
default:
return nil, err
}
return &groupMember, nil
}
type (
actionFn func(m *group.GroupInfo)
error
groupPolicyActionFn func(m *group.GroupPolicyInfo)
error
)
/ doUpdateGroupPolicy first makes sure that the group policy admin initiated the group policy update,
/ before performing the group policy update and emitting an event.
func (k Keeper)
doUpdateGroupPolicy(ctx sdk.Context, reqGroupPolicy, reqAdmin string, action groupPolicyActionFn, note string)
error {
groupPolicyAddr, err := k.accKeeper.AddressCodec().StringToBytes(reqGroupPolicy)
if err != nil {
return errorsmod.Wrap(err, "group policy address")
}
_, err = k.accKeeper.AddressCodec().StringToBytes(reqAdmin)
if err != nil {
return errorsmod.Wrap(err, "group policy admin")
}
groupPolicyInfo, err := k.getGroupPolicyInfo(ctx, reqGroupPolicy)
if err != nil {
return errorsmod.Wrap(err, "load group policy")
}
/ Only current group policy admin is authorized to update a group policy.
if reqAdmin != groupPolicyInfo.Admin {
return errorsmod.Wrap(sdkerrors.ErrUnauthorized, "not group policy admin")
}
if err := action(&groupPolicyInfo); err != nil {
return errorsmod.Wrap(err, note)
}
if err = k.abortProposals(ctx, groupPolicyAddr); err != nil {
return err
}
if err = ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroupPolicy{
Address: groupPolicyInfo.Address
}); err != nil {
return err
}
return nil
}
/ doUpdateGroup first makes sure that the group admin initiated the group update,
/ before performing the group update and emitting an event.
func (k Keeper)
doUpdateGroup(ctx sdk.Context, groupID uint64, reqGroupAdmin string, action actionFn, errNote string)
error {
groupInfo, err := k.getGroupInfo(ctx, groupID)
if err != nil {
return err
}
if !strings.EqualFold(groupInfo.Admin, reqGroupAdmin) {
return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "not group admin; got %s, expected %s", reqGroupAdmin, groupInfo.Admin)
}
if err := action(&groupInfo); err != nil {
return errorsmod.Wrap(err, errNote)
}
if err := ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroup{
GroupId: groupID
}); err != nil {
return err
}
return nil
}
/ assertMetadataLength returns an error if given metadata length
/ is greater than a pre-defined maxMetadataLen.
func (k Keeper)
assertMetadataLength(metadata, description string)
error {
if metadata != "" && uint64(len(metadata)) > k.config.MaxMetadataLen {
return errorsmod.Wrapf(errors.ErrMaxLimit, description)
}
return nil
}
/ validateDecisionPolicies loops through all decision policies from the group,
/ and calls each of their Validate()
method.
func (k Keeper)
validateDecisionPolicies(ctx sdk.Context, g group.GroupInfo)
error {
it, err := k.groupPolicyByGroupIndex.Get(ctx.KVStore(k.key), g.Id)
if err != nil {
return err
}
defer it.Close()
for {
var groupPolicy group.GroupPolicyInfo
_, err = it.LoadNext(&groupPolicy)
if errors.ErrORMIteratorDone.Is(err) {
break
}
if err != nil {
return err
}
err = groupPolicy.DecisionPolicy.GetCachedValue().(group.DecisionPolicy).Validate(g, k.config)
if err != nil {
return err
}
}
return nil
}
/ validateProposers checks that all proposers addresses are valid.
/ It as well verifies that there is no duplicate address.
func (k Keeper)
validateProposers(proposers []string)
error {
index := make(map[string]struct{
}, len(proposers))
for _, proposer := range proposers {
if _, exists := index[proposer]; exists {
return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", proposer)
}
_, err := k.accKeeper.AddressCodec().StringToBytes(proposer)
if err != nil {
return errorsmod.Wrapf(err, "proposer address %s", proposer)
}
index[proposer] = struct{
}{
}
}
return nil
}
/ validateMembers checks that all members addresses are valid.
/ additionally it verifies that there is no duplicate address
/ and the member weight is non-negative.
/ Note: in state, a member's weight MUST be positive. However, in some Msgs,
/ it's possible to set a zero member weight, for example in
/ MsgUpdateGroupMembers to denote that we're removing a member.
/ It returns an error if any of the above conditions is not met.
func (k Keeper)
validateMembers(members []group.MemberRequest)
error {
index := make(map[string]struct{
}, len(members))
for _, member := range members {
if _, exists := index[member.Address]; exists {
return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", member.Address)
}
_, err := k.accKeeper.AddressCodec().StringToBytes(member.Address)
if err != nil {
return errorsmod.Wrapf(err, "member address %s", member.Address)
}
if _, err := math.NewNonNegativeDecFromString(member.Weight); err != nil {
return errorsmod.Wrap(err, "weight must be non negative")
}
index[member.Address] = struct{
}{
}
}
return nil
}
/ isProposer checks that an address is a proposer of a given proposal.
func isProposer(proposal group.Proposal, address string)
bool {
for _, proposer := range proposal.Proposers {
if proposer == address {
return true
}
}
return false
}
func validateMsgs(msgs []sdk.Msg)
error {
for i, msg := range msgs {
m, ok := msg.(sdk.HasValidateBasic)
if !ok {
continue
}
if err := m.ValidateBasic(); err != nil {
return errorsmod.Wrapf(err, "msg %d", i)
}
}
return nil
}
Copy
Ask AI
ctx.EventManager().EmitEvent(
sdk.NewEvent(eventType, sdk.NewAttribute(attributeKey, attributeValue)),
)
EventManager
is accessed via the Context
.
See the Msg
services concept doc for a more detailed
view on how to typically implement Events and use the EventManager
in modules.
Subscribing to Events
You can use CometBFT’s Websocket to subscribe to Events by calling thesubscribe
RPC method:
Copy
Ask AI
{
"jsonrpc": "2.0",
"method": "subscribe",
"id": "0",
"params": {
"query": "tm.event='eventCategory' AND eventType.eventAttribute='attributeValue'"
}
}
eventCategory
you can subscribe to are:
NewBlock
: Contains Events triggered duringBeginBlock
andEndBlock
.Tx
: Contains Events triggered duringDeliverTx
(i.e. transaction processing).ValidatorSetUpdates
: Contains validator set updates for the block.
state
package after a block is committed. You can get the
full list of Event categories on the CometBFT Go documentation.
The type
and attribute
value of the query
allow you to filter the specific Event you are looking for. For example, a Mint
transaction triggers an Event of type EventMint
and has an Id
and an Owner
as attributes
(as defined in the events.proto
file of the NFT
module).
Subscribing to this Event would be done like so:
Copy
Ask AI
{
"jsonrpc": "2.0",
"method": "subscribe",
"id": "0",
"params": {
"query": "tm.event='Tx' AND mint.owner='ownerAddress'"
}
}
ownerAddress
is an address following the AccAddress
format.
The same way can be used to subscribe to legacy events.
Default Events
There are a few events that are automatically emitted for all messages, directly frombaseapp
.
message.action
: The name of the message type.message.sender
: The address of the message signer.message.module
: The name of the module that emitted the message.
The module name is assumed by
baseapp
to be the second element of the
message route: "cosmos.bank.v1beta1.MsgSend" -> "bank"
. In case a module
does not follow the standard message path, (e.g. IBC), it is advised to keep
emitting the module name event. Baseapp
only emits that event if the module
have not already done so.