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)