10.3 Event queries

One of the challenges of interacting with smart contracts is computing the current state of a contract.

For example, the ERC20 token standard defines a “balanceOf” method for a given input address, but does not dictate how those balances are to be stored or computed. On a token smart contract with hundreds of thousands or millions of balances holders, it is impractical to call “getBalance” for all accounts on every block.

MultiBaas implements a general solution called “Event Queries”. They allow a user to define a set of events and how they should be enumerated or aggregated, for example a list of balances or the most recent status per address.

An Event Query consists of:

  • Events: A list of events to query e.g. "Transfer"
  • Group by: The field under which to aggregate the results e.g. "account"
  • Order by: The field under in which the results should be returned in ascending order by default e.g. "account"
    • This can be overridden with a url parameter e.g. ?order_by=balance-desc

Each Event is defined by:

  • Event Name: The name of the event
  • Select: A list of Fields to return from the event
  • Filter: A set of Rules used to include or exclude events in the query

A Field contains:

  • InputName: The optional name for the event input e.g. "tokens"
    • Note: This field exists for readability and is discarded in processing
  • Aggregator: An aggregate operation to perform on the field when combined with a Group by clause
    • Possible aggregators are: add, subtract, last, first, min, max
  • InputIndex: The required zero-indexed input number of the parameter e.g. 2
  • Alias: An optional name to use for the field instead of its ABI name e.g. "balance"

A Filter contains:

  • Rule: The type of filter to apply
    • Possible rules are: input, addresses.address, contracts.label, contracts.contract_name, addresses.label, and, or
    • The two special rules "and" and "or" apply when used with Children filters
  • Operator: The type of comparison to use
    • Possible operators are: equal, notEqual, lessThan, greaterThan, lessThanOrEqual, greaterThanOrEqual
  • Value: The value to compare against when combined with a rule and operator
  • Children: A set of filters combined when the rule is "and" or "or" using the corresponding boolean logic

ERC20 Example

Consider an ERC20 token smart contract "curvetoken".

If the following Transfer(address indexed _from, address indexed _to, uint256 _value) events are emitted by the contract:

  1. Transfer( 0x0, 0xA, 100 ) // A "Mint" action
  2. Transfer( 0xA, 0xB, 20 )
  3. Transfer( 0xB, 0xC, 5 )

Then the state of the contract, if we were to call balanceOf(owner) for each of owner = 0x0, 0xA, 0xB, and 0xC, would be:

owner (Ethereum address) balanceOf(owner)
0x0 0
0xA 80
0xB 15
0xC 5

Event Query API

To retrieve that data using an event query, we can define one using the API as follows:

Request
PUT .../queries/curvetoken_balance
{
  "events": [
    {
      "filter": {
        "rule": "And",
        "children": [
          {
            "rule": "addresses.label",
            "value": "curvetoken",
            "operator": "Equal"
          }
        ]
      },
      "select": [
        {
          "alias": "account",
          "inputName": "from",
          "inputIndex": 0
        },
        {
          "alias": "balance",
          "inputName": "tokens",
          "aggregator": "subtract",
          "inputIndex": 2
        }
      ],
      "eventName": "Transfer"
    },
    {
      "filter": {
        "rule": "And",
        "children": [
          {
            "rule": "addresses.label",
            "value": "curvetoken",
            "operator": "Equal"
          }
        ]
      },
      "select": [
        {
          "alias": "account",
          "inputName": "to",
          "inputIndex": 1
        },
        {
          "alias": "balance",
          "inputName": "tokens",
          "aggregator": "add",
          "inputIndex": 2
        }
      ],
      "eventName": "Transfer"
    }
  ],
  "groupBy": "account"
}
Response
{
    "status": 200,
    "message": "success"
}

We can then execute the query as follows:

Request
GET .../queries/curvetoken_balance/results
Response
{
    "status": 200,
    "message": "success",
    "result": {
        "rows": [
            {
                "account": "0x0",
                "balance": -100
            },
            {
                "account": "0xA",
                "balance": 80
            },
            {
                "account": "0xB",
                "balance": 15
            },
            {
                "account": "0xC",
                "balance": 10
            }
        ]
    }
}

Note the "zero" account has a balance of -100. This is a special case as we are simply adding or subtracting all transfers to and from the account.

You can also preview a query by making a POST request.

Request
POST .../queries
{
  "events": [
    {
      "eventName": "Transfer",
      "select": [
        {
          "inputIndex": 2,
          "inputName": "value"
        }
      ]
    }
  ]
}
Response
{
    "status": 200,
    "message": "success",
    "result": {
        "rows": [
            {
                "value": 100
            },
            {
                "value": 20
            },
            {
                "value": 5 
            }
        ]
    }
}

Event Query UI

Event queries are accessed in the UI via the Contracts > Event Queries menu.

The main screen of the event queries management UI displays a list of existing queries, a paginated sample of their results, and actions that can be taken.

Clicking Edit or New Event Query brings up the event query edit screen. The top half of the page allows the user to define the events that will be aggregated into the event query. For each event, one or more fields may be added. For each event query, a field may be selected to group by and order by.

When you update the query, a sample of the results will appear below the query editor.

You can also preview the generated JSON definition of the query by expanding the Query JSON section.