ParsiQL

ParsiQL is a domain-specific language which serves the main purpose of manipulating streams - the core concept behind ParsiQL. One can imagine the stream of events as an infinite sequence of structs. There are two categories of streams:

  • Native streams consist of produced native events: Transfers, Calls and Logs.

  • User streams are formed from native ones or other user streams.

New user stream is built on top of a source stream, that is either native or a user stream.

Event is a term that is mostly used to define the input and output of Smart-Trigger that processes them or other high-level logic. However, when programming Smart-Trigger with ParsiQL we use the term struct to be able to describe event’s shape (properties and according data types) and data it holds (property values).

Example:

stream YourUserStream
from SourceStream

Primitive types

Values that are supported by usual primitive types.

  • bool: true, false. Supported operators:

    • ! (logical negation)

    • && (logical conjunction, “and”)

    • || (logical disjunction, “or”)

    • ==, != (boolean equality and inequality)

  • int256 (signed 256 bit integer): -2^255 - 2^255 -1. Supported operators:

    • + (addition)

    • - (subtraction)

    • * (multiplication)

    • / (integer division rounded towards zero)

    • <, <=, ==, !=, >=, > (integer comparisons)

  • string: "anything between double quotes is an UTF-8 string". Supported operators:

    • + (concatenation)

In order to prevent string length exponential grow, one of the concatenation operands must be a string literal

  • ==, != (string equality and inequality)

  • address: 0x215d4e8096ca21529ebb7168a2223e1102a59cea. Address of either externally owned account or smart contract.

Addresses do not support any operators!

Complex types

  • Structs (User Data struct representation in ParsiQL): { prop1: value1, prop2: value2, ... }. Supported operators:

    • . (access struct’s property)

    • ==, != (struct equality and inequality)

Struct can be initialized only as a struct literal either by passing properties and according values or expressions that compute them

...
let score = 5
let obj = {score: 5, name: "name", someData: score * 2}
...
  • Arrays: [ value0, value1, ... ]. Supported operators:

    • [i] (access i-th element of an array; starts from 0)

Variable that references value of type array can be initialized only with an array literal either by passing values or expressions that compute them

...
let arr = [1, 2, 3*7]
...
  • in (check that provided value is in the array)

...
where @from in [0x692a70d2e424a56d2c6c27aa97d1a86395877b3a, MyEthereumAddress]
...
  • ==, != (equality and inequality)

Tables (User Data table representation in ParsiQL). Table row record is represented as a struct. Supported operators:

  • [address] (access table row with a primary key equal to the provided address)

...
emit {fromMyWallet: MyWalletsTable[@from], scoreOfMyWallet: MyWalletsTable[@from].score}
...
  • in (check that provided address is among the table primary keys)

...
where @code_address in MonitoredTokensTable
...

Variable that references values of type table cannot be initialized.

Event transportation

By default, the user stream is delivered as is through all Smart-Trigger delivery channels. Applied transformations can change the shape of the output event, their amount or data they contain. In order to create a more customized resulting stream of Smart-Trigger output events, the emit language construct is available. The stream of such events will be immediately set for delivery on all of the Smart-Trigger’s delivery channels.

stream SomeDataStream
from Transfers
process
emit {constant: 5, doubleValue: 2 * @value}
end

Accessing User Data

Before programming Smart-Triggers, the context of User Data is loaded. It allows referencing User Data variables from the ParsiQL code.

  • User Data variables of a primitive kind with a particular primitive type are represented as ParsiQL variables that hold values of according primitive types.

  • User Data variables of struct and table kinds are represented as ParsiQL variables that hold values of according complex types.

Primitive

stream WithdrawAlerts
from Transfers
where @from == MyEthereumAddress && @value >= WithdrawThreshold
process
emit {alert: WithdrawMessage}
end

Struct

stream WithdrawAlerts
from Transfers
where @from == MyEthereumConfig.MyEthereumAddress && @value >= MyEthereumConfig.WithdrawThreshold
process
emit {alert: MyEthereumConfig.WithdrawMessage}
end

Table

stream WithdrawAlerts
from Transfers
where @from in MyEthereumTable && @value >= MyEthereumTable[.from].WithdrawThreshold
process
emit {message: MyEthereumTable[@from].WithdrawMessage}
end

Transformations

New user stream is formed by applying a number of transformations to the source stream. Stream context is available inside each of transformations - meaning that one can access event’s properties. There are three types of transformations.

Filter

Filters drop any events from the source stream that do not satisfy the condition. The condition that reflects ones business logic is defined by the boolean expression following the where clause. In addition, filters can be chained.

An example for a chained filter that basically acts as a logical conjunction (&&):

stream YourUserStream
from Transfers
where @to in [0x692a70d2e424a56d2c6c27aa97d1a86395877b3a]
where (@value > 50 || @balance == 0)

There is one performance-related limitation that requires (for many Streams, but not all) the filter of a special form immediately after the native stream. Such filter’s condition must include at least one boolean expression called address limiter. Address limiter checks that native event’s address property is equal to or included in any set of your addresses. It is also important that address limiter is present in each of||(or) expression or at least in one of && (and) expression.

Those are all valid examples:

stream YourUserStream
from Transfers
where @from == myFirstAddress && @to == mySecondAddress
stream YourUserStream
from Transfers
where @to == 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
|| @origin in mySetOfAddresses && @value > 50
stream YourUserStream
from Transfers
where @to in [0x692a70d2e424a56d2c6c27aa97d1a86395877b3a] && (@value > 50 || @balance == 0)

Those are all invalid examples:

stream YourUserStream
from Transfers
stream YourUserStream
from Transfers
where @balance == 0
stream YourUserStream
from Transfers
where @to = 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a || @value > 50

Map

Map is a kind of transformation that is applied to each event of a source stream and produces a completely new event that can be either of other type or contain new data. In ParsiQL, map is a select clause that takes a struct that represents the transformed event from the source stream.

Examples:

stream YourUserStream
from Transfers
where @from == myFirstAddress
select { etherWentTo: @to, ofAmount: @value }

There are few syntactic sugar tricks that allow eliminating repetitiveness in defining the mapped event. Those tricks can also be used when initializing struct variables or emitting events

In case the new event must contain (besides new ones) some exact properties and their values of the previous event:

...
select { @to, @value }
...

If there is a need to contain all of the properties and their values of the previous event:

...
select {...}
process
let obj = {...}
end
...

If there is a need to append all of the properties and their values of the previous event:

...
select {..., anotherProp: 1} # prepend
select {yetAnotherProp:2, ...} # append
select {preProp:3, ..., postProp: 4} # insert
...

ParsiQL is a strongly typed language. Each event in the stream has its own same type that defines the type of the stream. By applying the map transformation, the type of each mapped event is changed and it leads to a whole new type of the stream, as a result.

Process

Process is the main transformation block that opens up possibilities for applying more complex logic. In ParsiQL, process block is different from other transformations, as it can be stateful, provides advanced language elements and allows producing child streams.

Local variables

Any declared variable also has to be initialized on the same line. Despite the fact that ParsiQL is a strongly and statically typed language, developers are not forced to specify a type of a variable.

An example to declare and initialize a variable:

stream WalletHealthCheck
from Transfers
where @to = 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
process
let message = "ALIVE"
emit { message: message }
end

By default, when one variable is assigned with the value of another - the reference to the value is copied. In order to prevent accident change of User Data values after assignment during execution, the value (that was modified) of User Data variable will stay unchanged, however the other variable will now reference a copy of this value with later modification. This safety technique is called copy-on-write.

...
process
# Both varA and MyConfig variables reference the same value of type struct.
let varA = MyConfig
# After that varA will reference a copy of value with someProp equal to 10
# and MyConfig will still reference the value with an unchanged someProp.
varA.someProp = 10
end
...

State

Process does not have to be stateful, but in case it is - the state must be initialized at the very beginning of the process block. Each variable has to be both declared using the state keyword and initialized with some value on the same line.

A simple example for tracking the number of incoming transfers:

stream TransfersCount
from Transfers
where @to = 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
process
state transfersCount = 0
transfersCount++
emit { transfersCount }
end

Child streams

Creating a user stream from a native one is simple enough and can cover most of the use cases. However, in case a more flexible approach is required, then ParsiQL provides the emit .. to statement inside the process block that acts similarly to the select one. By executing this line, the newly-formed event would end up being in the child stream.

F.A.Q.

A1. How can I set a smart contract for monitoring?

B1. In case smart contract has participated in a transfer, then the address of its code is accessible via the code_address property.

stream TokenTransfers
from Transfers
where .code_address = PRQ # User Data variable holding PRQ token address.

A2. In the Status Console, I receive “Smart-Trigger got executed without producing any events”. What does that mean?

B2. It means, that your Smart-Trigger got activated (it met the address limiting condition) but during execution has not emitted any resulting events (i.e. because of additional filtering)