Skip to content

Repository files navigation

MultiversX events notifier

The notifier service is a component that receives block events synchronously from multiversx observer nodes, and it forwards them to a subscribing component (via message queuing service or websockets).

Prerequisites

In order to be able to run the notifier proxy and receive data, one has to setup one or multiple observers. For running an observing squad, these docs cover the whole process.

There are two ways the observer nodes can push data to the notifier service:

  1. WebSocket Integration (Recommended): The observer node pushes events to the notifier over a persistent WebSocket connection. This is the newer, more efficient method. To use this, check the observer node config and enable HostDriversConfig. The required configs for launching an observer/s with a WS driver attached can be found here.

  2. HTTP POST Integration (Deprecated): The observer node makes separate HTTP POST requests to the notifier's API endpoints (/events/push, etc.) for every event. This integration is still available for backwards compatibility, but it will be deprecated in the future.

How to run

Using the cmd/notifier package as root, execute the following commands:

  • install go dependencies: go install
  • build executable: go build -ldflags="-X main.appVersion=$(git describe --tags --long --dirty)" -o event-notifier
  • run ./event-notifier

Or use the build script:

bash scripts/build.sh

This can also be done using a single command from Makefile:

# by default, rabbitmq type
# `run` command will also trigger make `build` command
make run

# specify notifier running mode (eq: rabbitmq, ws)
make run publisher_type=rabbitmq

CLI: run --help to get the command line parameters

./cmd/notifier/event-notifier --help

Prerequisites

Before launching the proxy (notifier) service, it has to be configured so that it runs with the correct config variables.

The main config file can be found here.

The supported config variables are:

  • Host: the host and port on which the http server listens on. Should be the same port as the one specified in the ProxyUrl described above.
  • Username: the username used to authorize an observer. Can be left empty for UseAuthorization = false on observer connector.
  • Password: the password used to authorize an observer. Can be left empty for UseAuthorization = false on observer connector.
  • CheckDuplicates: signals if the events received from observers have already been pushed to clients. Requires a Redis instance/cluster.
  • WithReadStateChanges: signals if read state changes operations will be handled by the notifier. Note: this requires read state changes to also be enabled on the observer nodes.

If observer connector is set to use BasicAuth with UseAuthorization = true, Username and Password has to be set here on events notifier, and Auth flag has to be enabled in api.toml config file for events path. For example:

[APIPackages.events]
    Routes = [
        { Name = "/push", Open = true, Auth = true },
        { Name = "/revert", Open = true, Auth = false },

After the configuration file is set up, the notifier instance can be launched.

There are multiple publishing options when starting notifier service:

  • ws: it will launch a websocket handler (check WebSockets section)
  • rabbitmq: it will set up a rabbitMQ client based on the RabbitMQ section from main config file (check RabbitMQ section)

Development setup

There is a development setup using docker containers (with docker compose) that can be used for this.

If it is important that no event is lost, the setup with rabbitmq (as messaging system) and redis (as locker service) is recommended (to make sure no duplicated events are being processed).

If you want to use a similar setup in production systems, make sure to check docker-compose.yaml file and set up proper infrastructure and security configurations

  • notifier mode can be launched as following (check Makefile for details):
# Starts setup with one notifier instance
make docker-new publisher_type=ws

# Stop notifier instance
make docker-stop

# Start notifier instance
make docker-start
  • rabbit-api mode can be launched as following (check Makefile for details): Manage RabbitMQ and Redis
# Starts setup with one notifier instance, redis sentinel setup and rabbitmq
make compose-new

# Stop all containers
make compose-stop

# Start start all containers
make compose-start

# Shutdown entire setup
make compose-rm

Start Notifier instance

make docker-new publisher_type=rabbitmq

API Endpoints

Depending on the observer integration method chosen, the notifier service will expose the following routes.

For WebSocket Integration (Observer -> Notifier): When using the WebSocket integration, the observer pushes all events through a single persistent WebSocket connection rather than relying on the HTTP routes below.

For HTTP POST Integration (Observer -> Notifier): The observer nodes will push events to these routes via consecutive HTTP requests:

  • /events/push (POST) -> it will handle all events for each round
  • /events/revert (POST) -> if there is a reverted block, the event will be pushed on this route
  • /events/finalized (POST) -> when the block has been finalized, the events will be pushed on this route

For Client Subscriptions (Notifier -> Consumer): If the service will be in "notifier" mode (using the ws publisher), it will expose an additional route for end consumers to receive data:

  • /hub/ws (GET) - this route can be used to manage the WebSocket connection (check websocket subscribing section for more details on this)

Redis

In this setup, Redis is used as a locker service. If CheckDuplicates config parameter is set to true notifier instance will check for duplicated events in locker service. Alternatively, this option can be set/unset via the binary flag called --check-duplicates.

Check Redis section from config in order to set up the available options.

RabbitMQ

If --api-type command line parameter is set to rabbit-api, the notifier instance will try to publish events to RabbitMQ. Check RabbitMQ section for config in order to set up the url and exchanges properly.

There are multiple exchanges that can be used, they can be found in the main config file in the RabbitMQ section. The data structures corresponding to these exchanges are defined in code in data/outport.go file.

Subscribing

Once the proxy is launched together with the observer/s, the driver's methods will be called.

Event types

There are multiple event types available, they can be found as constants in common package, constants. Below is the list of available event types together with their associated JSON payload structures.

  • all_events: Pushes all logs and events for a block.
{
  "hash": "blockHash1",
  "events": [
    {
      "address": "addr1",
      "identifier": "identifier",
      "topics": ["<< base64 encoded topic1 >>", "<< base64 encoded topic2 >>"],
      "data": "<< base64 encoded data >> ",
      ...
    }
  ]
}
  • block_events: Pushes block info alongside its logs and events.
{
  "hash": "blockHash1",
  "shardId": 1,
  "timestamp": 12345678,
  "timestampMs": 12345678000,
  "events": [
    {
      "address": "addr1",
      "identifier": "identifier",
      ...
    }
  ]
}
  • revert_events: Pushes information relating to a reverted block.
{
    "hash": "blockHash1",
    "nonce": 11,
    "round": 2,
    "epoch": 1,
    "shardId": 1,
    "timestamp": 12345678,
    "timestampMs": 12345678000
}
  • finalized_events: Pushes the hash of a finalized block.
{
    "hash": "blockHash"
}
  • block_txs: Pushes all transactions contained within a block.
{
  "hash": "blockHash1",
  "txs": {
    "txHash1": {      
        "Nonce": 123,
        "Round": 1,
        "Epoch": 1,
        ...
    }
  }
}
  • block_scrs: Pushes all smart contract results contained within a block.
{
  "hash": "blockHash1",
  "scrs": {
    "scrHash1": {      
        "Nonce": 123,
        ...
    }
  }
}
  • block_state_accesses: Pushes information regarding the state accesses (reads/writes) occurring within a block. (Requires WithReadStateChanges config enabled)
{
  "hash": "blockHash1",
  "shardID": 1,
  "timestampMs": 12345678000,
  "nonce": 123,
  "stateAccessesPerAccounts": {
       ...
  }
}

RabbitMQ

When using a setup with RabbitMQ you have to subscribe to each exchange separately. This can be handled via RabbitMQ Management UI platform.

WebSockets

In order for a consumer to subscribe, it needs to select the correct communication protocol and send a payload signalling the intention of subscribing. This will generate a subscription for that session.

There are two types of events:

  • Protocol based events, such as ESDTTransfer or NFTCreate
  • Smart contract based events. These are defined inside a smart contract. The event will automatically be assigned the smart contract address, and the identifier will be the function by which it was triggered.

Example:

{
  "address": "erd111",
  "identifier": "swapTokens",
  "topics": ["RUdMRA==", "RVRI"],
}

Note:

  • The address field is bech32 encoded with the tag erd.
  • Topics are base64 encoded and require custom filters for decoding/filtering.

The subscribe message should be sent in json format and has the following form:

{
  "subscriptionEntries": [
    {
      "address": "erd123",
      "identifier": "swapExact"
    },
    {
      "address": "erdqqq",
      "identifier": "setValue"
    }
  ]
}

Each subscription upon creation is assigned a MatchLevel:

  • Match all *. All events are broadcast.
  • Match by address. Events are filtered by address.
  • Match by address && identifier. Events are filtered by (address, identifier).
  • Match by topics. Filtering is done by topics, it currently requires custom filter implementation.

The MatchLevel is assigned using the input payload sent while subscribing. Examples:

  • Match *:
{
  "subscriptionEntries": []
}
  • Match address:
{
  "subscriptionEntries": [
    {
      "address": "erdFirst"
    },
    {
      "address": "erdSecond"
    }
  ]
}
  • Match address && identifier:
{
  "subscriptionEntries": [
    {
      "address": "erdFirst",
      "identifier": "ESDTTransfer"
    },
    {
      "address": "erdSecond",
      "identifier": "setValue"
    }
  ]
}

The subscription entry has also a field for specifying event type, which can be one of the followings: all_events, revert_events, finalized_events. By default, it is set to all_events, for backwards compatibility reasons.

All other fields, like address, identifier, topics can be used for all_events. The other events type (revert_events and finalized_events) do not have these fields associated with them.

A subscription example with eventType will be like this:

{
  "subscriptionEntries": [
    {
      "eventType": "all_events",
      "address": "erdFirst",
      "identifier": "ESDTTransfer"
    },
    {
      "eventType": "finalized_events",
    }
  ]
}

Note: if eventType type is not specified, it will be set to all_events by default.

The payload data will consist of a marshalled object containing the event type and the inner marshalled data like:

{
  "type": "all_events",
  "data": "<< marshalled object here >>",
}

About

MultiversX notifier driver for events broadcasting

Resources

Stars

7 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages