Skip to content

Reporting & Analytics

Introduction

Cirrus is a STRATO component that allows for rapid retrieval of blockchain contract data. Cirrus provides secure, read-only access to indexed data in a tabular format according to a contract's name. It allows for users to make powerful requests to the API by using SQL-like queries that will be familiar to business and technical users alike.

Cirrus is what enables easy access to information like contract state data, Contract History/Audit Log, and Solidity Event Logs.

Support for EVM contracts in Cirrus is being deprecated

Starting in STRATO v7.5, the indexing of contracts which use the Ethereum Virtual Machine (EVM) is deprecated. By default STRATO will not index these contracts. Long term support for this feature should be expected and should be expected to be completely removed in future versions.

Developers have the option to enable EVM indexing on the expense of higher memory usage and slower overall performance. To enable this feature, use the flag:

--indexEVM=true

when starting STRATO.

Visit the Pluggable VM page for more information on the difference between the two VMs.

Data Indexing

All state variables from a contract are recorded as a separate column within a contract's table. A contract's name is its corresponding Cirrus table name. Along with state data, Cirrus records contract and transaction metadata for each update. Every unique contract account (address and chain ID pair) will be a separate entry in the table.

Inherited Columns

All contracts indexed in Cirrus have the following fields:

  • address
    • The address of this contract.
  • chainId
    • The chain ID of the chain that this contract is on.
    • Empty string if this contract is on the Main Chain.
  • record_id
    • The address appended by the chain ID with a colon (":").
    • If the chain ID is empty, than this is just the address of this contract.
    • Used to provide a unique ID for each row in the contract index table.
  • block_hash
    • The blockhash of the block where this contract was last updated.
  • block_timestamp
    • The block timestamp of the block where this contract was last updated.
    • Format: YYYY-MM-DD HH:MM:SS UTC
  • block_number
    • The block number of the block where this contract was last updated.
  • transaction_hash
    • The hash of the transaction in which this contract was last updated.
  • transaction_sender
    • The STRATO address of the account that sent the transaction in which this contract was last updated.

transaction_function_name is deprecated and removed

Contract tables in STRATO v7.2 and older contained the column "transaction_function_name" which was removed in v7.3

All instances of the same contract will exist within the same Cirrus table. This makes it easy to search across all occurrences of a contract to allow for rich data analytics and retrieval.

Contracts deployed to private chains are also indexed in Cirrus within the corresponding contract table.

Data Types

Values of variables that are of complex data types are not represented in Cirrus since they are not "flat" data types. These include arrays, structs, mappings and enums. Below is a table of data types in Solidity and how the corresponding type is represented in Cirrus:

Solidity Type Cirrus Type/Format
string string
uint/int integer
bytes string
bool boolean
enum integer - Index of enum value
address string of address
account(address) string of address
account(address, chainId) string of <address>:<chainId>
Contract(address) string of contract <address>
Contract(account(address, chainId)) string of contract <address>:<chainId>*
new Contract(args) string of contract <address>
array Not Indexed
struct Not Indexed
mapping(<T> => <V>) "0000000000000000000000000000000000000000"

Note

Any chain ID in an account-type that resolves to the Main Chain will be recorded as <address>:main.

  • Any Contract type that references a non-existent contract will be recorded as NULL.

As an example, let's say there is a smart contract as shown below:

contract SimpleStorage {

  enum Decision { YES, NO, MAYBE }
  string message;
  uint number;
  bool isEven;
  account myAccount;
  int[] myArr;
  Decision myDecision;

  constructor(string _message, uint _number, address _address, uint _chainId, int[] _myArr, Decision _decision) {
    message = _message;
    number = _number;
    isEven = _number % 2 == 0;
    myAccount = account(_address, _chainId);
    myArr = _myArr;
    myDecision = _decision;
  }
}

If two instances of this contract were created with different values, the corresponding SimpleStorage table would contain the following data (block and transaction data not shown.)

SimpleStorage

...defaultColumns message (string) number (integer) isEven (boolean) myAccount (string) myDecision (integer)
...values "Hello World" 4 true "...0000123 : ...0000456" 0
...values "Good Morning" 37 false "...0000abc : ...0000def" 1

The results from the query are returned from the API as an array of JSON objects. Each element of the array is a row from the query. Each column returned from the query is a property of the object.

Returned Data

[
  {
    "address": "<contract-address>",
    "chainId": "<chain-id>",
    "record_id": "<contract-address>:<chain-id>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Hello World",
    "number": 4,
    "isEven": true,
    "myAccount": "0000000000000000000000000000000000000123:0000000000000000000000000000000000000000000000000000000000000456",
    "myDecision": 0
  },
]

Table Naming

A contract's table name as stored in Cirrus is prepended by the organization name of the user who created the contract, and additionally the application contract name that created this contract instance.

See the X.509 & User Identity Documentation for more details.

Querying Cirrus

STRATO uses the PostgREST v9.0 API to allow users to interface with the Cirrus database. The PostgREST API allows users to write SQL-like queries directly as query parameters in a STRATO API call. Because there are many powerful functions of PostgREST, this guide will only serve to show the most common and basic requests possible. When using the BlockApps Rest SDK, query parameters are given via the query property of the options object. Each key-value of the query object is the corresponding key-value of the Cirrus API request.

The Cirrus endpoint on STRATO is:

GET https://<strato_address>/cirrus/search/<contract-name>

Select Columns

You may select a subset of rows to retrieve from Cirrus. By default all rows are returned.

This is done by using the select query parameter. The columns to be returned are a comma separated string.

Example

curl -X GET \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
"https://<strato_address>/cirrus/search/SimpleStorage?select=address,message,isEven"
const query = {
  select: 'address,message,isEven'
}

const contract = {
  name: "SimpleStorage"
}

const newOptions = {
  ...options,
  query
}
const rows = await rest.search(stratoUser, contract, newOptions)

Response

[
  {
    "address": "<contract-address>",
    "message": "Hello World",
    "isEven": "true"
  },
  {
    "address": "<contract-address>",
    "message": "Good Morning",
    "isEven": "false"
  }
]

Filter Rows by Values

Filter the results by using a column's name and a condition that needs to be met for it to be returned. A filter takes the format of:

?<column-name>=<operator>.<value>

An operator is some type of logical qualifier in relation to the provided value. PostgREST uses the following reserved keywords for operators:

  • eq - equal
    • columnValue == value
  • neq - not equal
    • columnValue != value
  • lt - less than
    • columnValue < value
  • lte - less than or equal
    • columnValue <= value
  • gt - greater than
    • columnValue > value
  • gte - greater than or equal
    • columnValue >= value
  • not - negation
    • !(condition)
    • Used as a prefix to negate the proceeding operator.
  • like
    • Case-sensitive text match. SQL like operator.
    • Use * as a wildcard to match zero or more characters.
    • Does not use regular expression pattern matching.
    • "blockapps" like "BlockApps" = false
  • ilike
    • Case-insensitive text match. SQL ilike operator.
    • Use * as a wildcard to match zero or more characters.
    • Does not use regular expression pattern matching.
    • "blockapps" ilike "BlockApps" = true

See the PostgREST Filters for a complete list of supported filters.

Filters can also be used in conjunction with each other by using the or and and operators to form a clause.

  • and
    • Returns true if all of the conditions within it are true.
    • condition1 && condition2
  • or
    • Returns true if any of conditions within it are true.
    • condition1 || condition2

Each filter within an and/or clause must be separated by a comma, and the entire clause must be wrapped in parentheses. The separator between a column name and condition must now be a period (.):

?<operator>=(<column-name>.<operator>.<value>,<column-name2>.<operator2>.<value2>)

Like other conditions, these conditions may also be prefixed by the not operator to negate the entire statement.

Conjunction operators may contain other conjunctions:

?or=(and(<column-name>.<operator>.<value>,<column-name2>.<operator2>.<value2>),<column-name3>.<operator3>.<value3>)

// equivalent logical statement

(condition1 && condition2) || condition3

An and condition on separate columns is equivalent to declaring separate filter conditions on each column:

?and=(message.eq.Hello,number.gt.10)

is equivalent to:

?message=eq.Hello&number=gt.10

Example

Return all rows where message contains the string "Morning" or number is greater than 20, and the contract is on the Main Chain (chain ID is the empty string).

curl -X GET \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
"https://<strato_address>/cirrus/search/SimpleStorage?or=(message.like.*Morning*,number.gt.20)&chainId=eq."
const query = {
  or: '(message.like.*Morning*,number.gt.20)',
  chainId: 'eq.'
}

const newOptions = {
  ...options,
  query
}
const contract = {
  name: "SimpleStorage"
}

const rows = await rest.search(stratoUser, contract, newOptions)

Returns

[
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Good Morning",
    "number": 37,
    "isEven": false,
    "myAccount": "0000000000000000000000000000000000000abc:0000000000000000000000000000000000000000000000000000000000000def",
    "badArr": "0000000000000000000000000000000000000000"
  },
]

Sort Rows

Sort the returned rows by column value.

?order=<column-name>.<ordering>

Multiple columns can be sorted on by specifying them as a comma-separated list. This allows rows with the same value of a column to fall back to the next sorting order.

?order=<column-name>.<ordering>,<column-name2>.<ordering2>

Ordering uses the following keywords:

  • asc - default
    • Ascending order
    • 0, 1, 2, 3...
  • desc
    • Descending order
    • 3, 2, 1, 0...

Example

Sort results by the number column in descending order.

curl -X GET \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
"https://<strato_address>/cirrus/search/SimpleStorage?order=number.desc"
const query = {
  order: 'number.desc',
}

const newOptions = {
  ...options,
  query
}
const contract = {
  name: "SimpleStorage"
}

const rows = await rest.search(stratoUser, contract, newOptions)

Returns

[
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Good Morning",
    "number": 37,
    "isEven": false,
    "myAccount": "0000000000000000000000000000000000000abc:0000000000000000000000000000000000000000000000000000000000000def",
    "badArr": "0000000000000000000000000000000000000000"
  },
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Good Morning",
    "number": 4,
    "isEven": true,
    "myAccount": "0000000000000000000000000000000000000123:0000000000000000000000000000000000000000000000000000000000000456",
    "badArr": "0000000000000000000000000000000000000000"
  },
]

Limit & Offset Results

If there are many instances of a single contract type, than it may be useful to only retrieve a subset of entries from the full table. This feature is complemented with offsetting, which allows the returned results to begin after a given offset.

Limit will restrict the results to contain a maximum of limit entries.

Offset will start returned results from the entry at the given offset (zero-indexed). If the offset is greater than or equal to the number of entries in the table, no results will be returned

Limit and offset use the same corresponding query parameters:

?limit=<limit>&offset=<offset>

Example

Retrieve a maximum of 50 entries, returned rows will begin from the 50th entry (first entry is the 0th entry).

curl -X GET \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
"https://<strato_address>/cirrus/search/SimpleStorage?limit=50&offset=50"
const query = {
  limit: 50,
  offset: 50
}

const newOptions = {
  ...options,
  query
}
const contract = {
  name: "SimpleStorage"
}

const rows = await rest.search(stratoUser, contract, newOptions)

Returns

[
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Good Evening",
    "number": 14,
    "isEven": true,
    "myAccount": "00000000000000000000000000000000001a2b3c:0000000000000000000000000000000000000000000000000000000000006c5e",
    "badArr": "0000000000000000000000000000000000000000"
  },
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "How do you do?",
    "number": 55,
    "isEven": false,
    "myAccount": "0000000000000000000000000000000000e523ac:00000000000000000000000000000000000000000000000000000000000765",
    "badArr": "0000000000000000000000000000000000000000"
  },
  {
    "address": "<contract-address>",
    "chainId": "",
    "record_id": "<contract-address>",
    "block_hash": "<block-hash>",
    "block_timestamp": "<block-timestamp>",
    "block_number": "<block-number>",
    "transaction_hash": "<transaction-hash>",
    "transaction_sender": "<transaction-sender-address>",
    "message": "Good Afternoon",
    "number": 12,
    "isEven": true,
    "myAccount": "0000000000000000000000000000000003ba144d:0000000000000000000000000000000000000000000000000000002b1f52cced",
    "badArr": "0000000000000000000000000000000000000000"
  },
  // ... 47 more entries
]

Joining Multiple Tables

SQL allows for two or more tables to be joined together on common values from their columns to form a single table with the contents of each individual table together. In Cirrus, this can be done with contracts that contain address references to other contracts in a state variable. This allows for powerful data querying between both contract's index tables in a single query to Cirrus. This feature is useful when users need to isolate data about an object to a single contract, rather than including data that does not necessarily relate to what this contract is describing. This helps developers model their contracts in a relational model, following the "Normal Forms" commonly used in database design Database Normalization.

For security purposes, it is required that the type of the variable in a contract must be the name of the foreign contract. A state variable with the generic account or address type cannot be used in table joins.

Example

As an example, we have the following contracts Book and Author:

contract Author {
  string first_name;
  string last_name;
  string birth_date;
  constructor(string _first_name, string _last_name, string _birth_date) {
    first_name = _first_name;
    last_name = _last_name;
    birth_date = _birth_date;
  }
}

contract Book {
  Author author;
  string name;
  string publish_date;
  constructor(string _name, string _publish_date, address _author) {
    name = _name;
    publish_date = _publish_date;
    author = Author(account(_author));
  }
}

To perform a join query between the Book and Author contract, query the Book cirrus table as normally, and in the select query parameter, add the "Author" contract name, followed by the columns in Author that you wish to include in the response. Use the wildcard "*" to select all of the columns of single table.

GET https://<strato_address>/cirrus/search/Book?select=name,publish_date,Author(first_name,last_name)
  curl -X GET \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" \
  "https://<strato_address>/cirrus/search/Book?select=name,publish_date,Author(first_name,last_name)"
const query = {
  select: "name,publish_date,Author(first_name,last_name)",
}

const newOptions = {
  ...options,
  query
}
const contract = {
  name: "Book"
}

const rows = await rest.search(stratoUser, contract, newOptions)

Response

[
  {
    "name": "The Sea, the Sea",
    "publish_date": "1978",
    "Author": {
      "first_name": "Iris",
      "last_name": "Murdoch"
    }
  },
  {
    "name": "Never Let Me Go",
    "publish_date": "2005",
    "Author": {
      "first_name": "Kazuo",
      "last_name": "Ishiguro"
    }
  }
]

See the PostgREST documentation on Resource Embedding for further information.

Row Count

Easily query the number of entries in a table for analytics by selecting the count of the returned rows from a query.

Use the following query parameters to get the count of rows:

?select=count

This query can be used in conjunction with other filters to select the count of rows that match that filter.

Example

Retrieve the number of entries in a table where number is less than 25.

curl -X GET \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
"https://<strato_address>/cirrus/search/SimpleStorage?select=count&number=lt.25"
const query = {
  select: 'count',
  number: 'lt.25',
}

const newOptions = {
  ...options,
  query
}
const contract = {
  name: "SimpleStorage"
}

const rows = await rest.search(stratoUser, contract, newOptions)

Returns

The return result for a count query is an array with a single object with they key count.

[
  {
    "count": 14, 
  }
]

PostgREST Reference

For a complete usage guide to the PostgREST v9.0 API, visit their documentation page.

Event Logs

Contracts written in SolidVM allow Cirrus to log Solidity Events with the exact data with which they were emitted. This is in great contrast to EVM, where events are not human readable and hard to access, due to the nature of how they are implemented. Recording events and their arguments in SolidVM allows them to be used as lightweight pseudo-contracts. They can be easily accessed from Cirrus and do not affect the state of the blockchain. They are useful for recording read-only data.

Info

Events are stored in Cirrus with the naming format of <ContractName>.<EventName>.

As an example, below is a SolidVM contract that emits an event upon contract creation and then the event information is retrieved from Cirrus.

contract EmitEvent {
  event MyEvent(string memo, uint timestamp, bool myBool);
  constructor(string m, uint t, bool b) {
    emit MyEvent(m, t, b);
  }
}

To query for an event in Cirrus, make the following API call:

Request

curl -X GET \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json" \
  "https://<strato_address>/cirrus/search/EmitEvent.MyEvent"
Returns
[
  {
    "id": 1,
    "address": "a1b2c3d4e5f67a1b2c3d4e5f67a1b2c3d4e5f67a",
    "memo": "Hello World",
    "timestamp": "12345",
    "myBool": "True"
  }
]

Info

Cirrus always stores Data from Solidity Events as strings, so remember to convert them to their proper types when working with event data through the API.

Check out our VS Code extension! Signup for STRATO Mercata