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. 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.

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 account information. Every unique contract account (address and chain ID pair) will be a separate entry in the table.

  • 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.

All contract tables also contain columns for transaction and block information. The recorded values are always the most up-to-date from the latest transaction involving this contract account:

  • block_hash
  • block_timestamp
    • Format: YYYY-MM-DD HH:MM:SS UTC
  • block_number
  • transaction_hash
  • transaction_sender

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.

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
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 "0000000000000000000000000000000000000000"
struct "0000000000000000000000000000000000000000"
mapping(<T> => <V>) "0000000000000000000000000000000000000000"
enum "0"

Note

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

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

contract SimpleStorage {
  string message;
  uint number;
  bool isEven;
  account myAccount;
  int[] badArr;
  constructor(string _message, uint _number, address _address, uint _chainId, int[] _badArr) {
    message = _message;
    number = _number;
    isEven = _number % 2 == 0;
    myAccount = account(_address, _chainId);
    badArr = _badArr;
  }
}

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

address chainId message number isEven myAccount badArr
"1e6cdfd5bb2..." "" "Hello World" 4 true "...0000123 : ...0000456" "0000..."
"172bafcdf98..." "" "Good Morning" 37 false "...0000abc : ...0000def" "0000.."

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>",
    "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",
    "badArr": "0000000000000000000000000000000000000000"
  },
]

Querying Cirrus

STRATO uses the PostgREST v4.3 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": "",
    "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": "",
    "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": "",
    "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": "",
    "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": "",
    "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": "",
    "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
]

PostgREST Reference

For a complete usage guide to the PostgREST v4.3 API, visit their documentation page.