github.com/cosmos/cosmos-sdk@v0.50.10/docs/architecture/adr-032-typed-events.md (about) 1 # ADR 032: Typed Events 2 3 ## Changelog 4 5 * 28-Sept-2020: Initial Draft 6 7 ## Authors 8 9 * Anil Kumar (@anilcse) 10 * Jack Zampolin (@jackzampolin) 11 * Adam Bozanich (@boz) 12 13 ## Status 14 15 Proposed 16 17 ## Abstract 18 19 Currently in the Cosmos SDK, events are defined in the handlers for each message as well as `BeginBlock` and `EndBlock`. Each module doesn't have types defined for each event, they are implemented as `map[string]string`. Above all else this makes these events difficult to consume as it requires a great deal of raw string matching and parsing. This proposal focuses on updating the events to use **typed events** defined in each module such that emiting and subscribing to events will be much easier. This workflow comes from the experience of the Akash Network team. 20 21 ## Context 22 23 Currently in the Cosmos SDK, events are defined in the handlers for each message, meaning each module doesn't have a cannonical set of types for each event. Above all else this makes these events difficult to consume as it requires a great deal of raw string matching and parsing. This proposal focuses on updating the events to use **typed events** defined in each module such that emiting and subscribing to events will be much easier. This workflow comes from the experience of the Akash Network team. 24 25 [Our platform](http://github.com/ovrclk/akash) requires a number of programatic on chain interactions both on the provider (datacenter - to bid on new orders and listen for leases created) and user (application developer - to send the app manifest to the provider) side. In addition the Akash team is now maintaining the IBC [`relayer`](https://github.com/ovrclk/relayer), another very event driven process. In working on these core pieces of infrastructure, and integrating lessons learned from Kubernetes developement, our team has developed a standard method for defining and consuming typed events in Cosmos SDK modules. We have found that it is extremely useful in building this type of event driven application. 26 27 As the Cosmos SDK gets used more extensively for apps like `peggy`, other peg zones, IBC, DeFi, etc... there will be an exploding demand for event driven applications to support new features desired by users. We propose upstreaming our findings into the Cosmos SDK to enable all Cosmos SDK applications to quickly and easily build event driven apps to aid their core application. Wallets, exchanges, explorers, and defi protocols all stand to benefit from this work. 28 29 If this proposal is accepted, users will be able to build event driven Cosmos SDK apps in go by just writing `EventHandler`s for their specific event types and passing them to `EventEmitters` that are defined in the Cosmos SDK. 30 31 The end of this proposal contains a detailed example of how to consume events after this refactor. 32 33 This proposal is specifically about how to consume these events as a client of the blockchain, not for intermodule communication. 34 35 ## Decision 36 37 **Step-1**: Implement additional functionality in the `types` package: `EmitTypedEvent` and `ParseTypedEvent` functions 38 39 ```go 40 // types/events.go 41 42 // EmitTypedEvent takes typed event and emits converting it into sdk.Event 43 func (em *EventManager) EmitTypedEvent(event proto.Message) error { 44 evtType := proto.MessageName(event) 45 evtJSON, err := codec.ProtoMarshalJSON(event) 46 if err != nil { 47 return err 48 } 49 50 var attrMap map[string]json.RawMessage 51 err = json.Unmarshal(evtJSON, &attrMap) 52 if err != nil { 53 return err 54 } 55 56 var attrs []abci.EventAttribute 57 for k, v := range attrMap { 58 attrs = append(attrs, abci.EventAttribute{ 59 Key: []byte(k), 60 Value: v, 61 }) 62 } 63 64 em.EmitEvent(Event{ 65 Type: evtType, 66 Attributes: attrs, 67 }) 68 69 return nil 70 } 71 72 // ParseTypedEvent converts abci.Event back to typed event 73 func ParseTypedEvent(event abci.Event) (proto.Message, error) { 74 concreteGoType := proto.MessageType(event.Type) 75 if concreteGoType == nil { 76 return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type) 77 } 78 79 var value reflect.Value 80 if concreteGoType.Kind() == reflect.Ptr { 81 value = reflect.New(concreteGoType.Elem()) 82 } else { 83 value = reflect.Zero(concreteGoType) 84 } 85 86 protoMsg, ok := value.Interface().(proto.Message) 87 if !ok { 88 return nil, fmt.Errorf("%q does not implement proto.Message", event.Type) 89 } 90 91 attrMap := make(map[string]json.RawMessage) 92 for _, attr := range event.Attributes { 93 attrMap[string(attr.Key)] = attr.Value 94 } 95 96 attrBytes, err := json.Marshal(attrMap) 97 if err != nil { 98 return nil, err 99 } 100 101 err = jsonpb.Unmarshal(strings.NewReader(string(attrBytes)), protoMsg) 102 if err != nil { 103 return nil, err 104 } 105 106 return protoMsg, nil 107 } 108 ``` 109 110 Here, the `EmitTypedEvent` is a method on `EventManager` which takes typed event as input and apply json serialization on it. Then it maps the JSON key/value pairs to `event.Attributes` and emits it in form of `sdk.Event`. `Event.Type` will be the type URL of the proto message. 111 112 When we subscribe to emitted events on the CometBFT websocket, they are emitted in the form of an `abci.Event`. `ParseTypedEvent` parses the event back to it's original proto message. 113 114 **Step-2**: Add proto definitions for typed events for msgs in each module: 115 116 For example, let's take `MsgSubmitProposal` of `gov` module and implement this event's type. 117 118 ```protobuf 119 // proto/cosmos/gov/v1beta1/gov.proto 120 // Add typed event definition 121 122 package cosmos.gov.v1beta1; 123 124 message EventSubmitProposal { 125 string from_address = 1; 126 uint64 proposal_id = 2; 127 TextProposal proposal = 3; 128 } 129 ``` 130 131 **Step-3**: Refactor event emission to use the typed event created and emit using `sdk.EmitTypedEvent`: 132 133 ```go 134 // x/gov/handler.go 135 func handleMsgSubmitProposal(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgSubmitProposalI) (*sdk.Result, error) { 136 ... 137 types.Context.EventManager().EmitTypedEvent( 138 &EventSubmitProposal{ 139 FromAddress: fromAddress, 140 ProposalId: id, 141 Proposal: proposal, 142 }, 143 ) 144 ... 145 } 146 ``` 147 148 ### How to subscribe to these typed events in `Client` 149 150 > NOTE: Full code example below 151 152 Users will be able to subscribe using `client.Context.Client.Subscribe` and consume events which are emitted using `EventHandler`s. 153 154 Akash Network has built a simple [`pubsub`](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/pubsub/bus.go#L20). This can be used to subscribe to `abci.Events` and [publish](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/events/publish.go#L21) them as typed events. 155 156 Please see the below code sample for more detail on this flow looks for clients. 157 158 ## Consequences 159 160 ### Positive 161 162 * Improves consistency of implementation for the events currently in the Cosmos SDK 163 * Provides a much more ergonomic way to handle events and facilitates writing event driven applications 164 * This implementation will support a middleware ecosystem of `EventHandler`s 165 166 ### Negative 167 168 ## Detailed code example of publishing events 169 170 This ADR also proposes adding affordances to emit and consume these events. This way developers will only need to write 171 `EventHandler`s which define the actions they desire to take. 172 173 ```go 174 // EventEmitter is a type that describes event emitter functions 175 // This should be defined in `types/events.go` 176 type EventEmitter func(context.Context, client.Context, ...EventHandler) error 177 178 // EventHandler is a type of function that handles events coming out of the event bus 179 // This should be defined in `types/events.go` 180 type EventHandler func(proto.Message) error 181 182 // Sample use of the functions below 183 func main() { 184 ctx, cancel := context.WithCancel(context.Background()) 185 186 if err := TxEmitter(ctx, client.Context{}.WithNodeURI("tcp://localhost:26657"), SubmitProposalEventHandler); err != nil { 187 cancel() 188 panic(err) 189 } 190 191 return 192 } 193 194 // SubmitProposalEventHandler is an example of an event handler that prints proposal details 195 // when any EventSubmitProposal is emitted. 196 func SubmitProposalEventHandler(ev proto.Message) (err error) { 197 switch event := ev.(type) { 198 // Handle governance proposal events creation events 199 case govtypes.EventSubmitProposal: 200 // Users define business logic here e.g. 201 fmt.Println(ev.FromAddress, ev.ProposalId, ev.Proposal) 202 return nil 203 default: 204 return nil 205 } 206 } 207 208 // TxEmitter is an example of an event emitter that emits just transaction events. This can and 209 // should be implemented somewhere in the Cosmos SDK. The Cosmos SDK can include an EventEmitters for tm.event='Tx' 210 // and/or tm.event='NewBlock' (the new block events may contain typed events) 211 func TxEmitter(ctx context.Context, cliCtx client.Context, ehs ...EventHandler) (err error) { 212 // Instantiate and start CometBFT RPC client 213 client, err := cliCtx.GetNode() 214 if err != nil { 215 return err 216 } 217 218 if err = client.Start(); err != nil { 219 return err 220 } 221 222 // Start the pubsub bus 223 bus := pubsub.NewBus() 224 defer bus.Close() 225 226 // Initialize a new error group 227 eg, ctx := errgroup.WithContext(ctx) 228 229 // Publish chain events to the pubsub bus 230 eg.Go(func() error { 231 return PublishChainTxEvents(ctx, client, bus, simapp.ModuleBasics) 232 }) 233 234 // Subscribe to the bus events 235 subscriber, err := bus.Subscribe() 236 if err != nil { 237 return err 238 } 239 240 // Handle all the events coming out of the bus 241 eg.Go(func() error { 242 var err error 243 for { 244 select { 245 case <-ctx.Done(): 246 return nil 247 case <-subscriber.Done(): 248 return nil 249 case ev := <-subscriber.Events(): 250 for _, eh := range ehs { 251 if err = eh(ev); err != nil { 252 break 253 } 254 } 255 } 256 } 257 return nil 258 }) 259 260 return group.Wait() 261 } 262 263 // PublishChainTxEvents events using cmtclient. Waits on context shutdown signals to exit. 264 func PublishChainTxEvents(ctx context.Context, client cmtclient.EventsClient, bus pubsub.Bus, mb module.BasicManager) (err error) { 265 // Subscribe to transaction events 266 txch, err := client.Subscribe(ctx, "txevents", "tm.event='Tx'", 100) 267 if err != nil { 268 return err 269 } 270 271 // Unsubscribe from transaction events on function exit 272 defer func() { 273 err = client.UnsubscribeAll(ctx, "txevents") 274 }() 275 276 // Use errgroup to manage concurrency 277 g, ctx := errgroup.WithContext(ctx) 278 279 // Publish transaction events in a goroutine 280 g.Go(func() error { 281 var err error 282 for { 283 select { 284 case <-ctx.Done(): 285 break 286 case ed := <-ch: 287 switch evt := ed.Data.(type) { 288 case cmttypes.EventDataTx: 289 if !evt.Result.IsOK() { 290 continue 291 } 292 // range over events, parse them using the basic manager and 293 // send them to the pubsub bus 294 for _, abciEv := range events { 295 typedEvent, err := sdk.ParseTypedEvent(abciEv) 296 if err != nil { 297 return er 298 } 299 if err := bus.Publish(typedEvent); err != nil { 300 bus.Close() 301 return 302 } 303 continue 304 } 305 } 306 } 307 } 308 return err 309 }) 310 311 // Exit on error or context cancelation 312 return g.Wait() 313 } 314 ``` 315 316 ## References 317 318 * [Publish Custom Events via a bus](https://github.com/ovrclk/akash/blob/90d258caeb933b611d575355b8df281208a214f8/events/publish.go#L19-L58) 319 * [Consuming the events in `Client`](https://github.com/ovrclk/deploy/blob/bf6c633ab6c68f3026df59efd9982d6ca1bf0561/cmd/event-handlers.go#L57)