Welcome to the Rell SDK¶
Introduction¶
Rell SDK¶
Welcome to the first Rell SDK! This is a milestone in the development of Chromia, and one of the first opportunities to get your hands on the latest tools that have been developed for the platform. In line with our mission to make mainstream dapps a practical reality, we have decided to prioritise providing a rich and usable toolset for dapp developers right from the outset.
This early SDK consists of our Rell IDE which allows you to write and compile Rell code, as well as language documentation, code samples, and a simple tutorial. We will be expanding this material as Rell matures and develops. Right now it gives the dapp developer community an early look at what they will be able to achieve with Chromia and Rell.
We welcome any and all feedback, please send bug reports and suggestions to dev_feedback@chromia.com
Chromia¶
Rell is built for Chromia. Chromia is a new blockchain platform for decentralized applications, conceived in response to the shortcomings of existing platforms and designed to enable a new generation of dapps to scale beyond what is currently possible
While platforms such as Ethereum allow any kind of application to be implemented in theory, in practice they have many limitations: bad user experience, high fees, frustrating developer experience, poor security. This prevents decentralized apps (dapps) from going mainstream.
We believe that to address these problems properly we need to seriously rethink the blockchain architecture and programming model with the needs of decentralized applications in mind. Our priorities are to:
- Allow dapps to scale to millions of users.
- Improve the user experience of dapps to achieve parity with centralized applications.
- Allow developers to build secure applications with using familiar paradigms.
Rell language¶
Most dapp blockchain platforms use virtual machines of various kinds. But a traditional virtual machine architecture doesn’t work very well with the Chromia relational data model, as we need a way to encode queries as well as operations. For this reason, we are taking a more language-centric approach: a new language called Rell (Relational language) will be used for dapp programming. This language allows programmers to describe the data model/schema, queries, and procedural application code.
Rell code is compiled to an intermediate binary format which can be understood as code for a specialized virtual machine. Chromia nodes will then translate queries contained in this code into SQL (while making sure this translation is safe) and execute code as needed using an interpreter or compiler.
Rell has the following features:
- Type safety / static type checks. It’s very important to catch programming errors at the compilation stage to prevent financial losses. Rell is much more type-safe than SQL, and it makes sure that types returned by queries match types used in procedural code.
- Safety-optimized. Arithmetic operations are safe right out of the box, programmers do not need to worry about overflows. Authorization checks are explicitly required.
- Concise, expressive and convenient. Many developers dislike SQL because it is highly verbose. Rell doesn’t bother developers with details which can be derived automatically. As a data definition language, Rell is up to 7x more compact than SQL.
- Allows meta-programming. We do not want application developers to implement the basics from scratch for every dapp. Rell will allow functionality to be bundled as templates.
Our research indicated that no existing language or environment has this feature set, and thus development of a new language was absolutely necessary.
We designed Rell in such a way that it is easy to learn for programmers:
- Programmers can use relational programming idioms they are already familiar with. However, they don’t have to go out of their way to express everything through relational algebra: Rell can seamlessly merge relational constructs with procedural programming.
- The language is deliberately similar to modern programming languages like JavaScript and Kotlin. A familiar language is easier to adapt to, and our internal tests show that programmers can become proficient in Rell in matter of days. In contrast, the ALGOL-style syntax of PL/SQL generally feels unintuitive to modern developers.
Quick start¶
This document should help you get up and running with the Rell IDE.
Install Docker¶
A software called Docker is required in order to run the IDE. It can be downloaded from docker.io, you will also have to create an account.
Install the IDE¶
This tutorial assumes that you are using Linux or a Mac OS, and that you have a certain familiarity with *nix operating systems and tools. It is possible to install the IDE on Windows, but we do not have specific instructions for doing so. We hope to add them in future.
- Create a new directory called
relltest
- Create a file called
docker-compose.yml
and paste the following into it:
# Copyright (c) 2017 ChromaWay Inc. See README for license information.
version: '3.1'
services:
postgres:
image: chromaway/postgres:2.4.3-beta
restart: always
environment:
POSTGRES_PASSWORD: postchain
dev-preview:
image: chromaway/dev-preview:0.7.0-b3
restart: always
ports:
- 127.0.0.1:7740:7740
- 127.0.0.1:30000:30000
depends_on:
- postgres
- Open a terminal in the
relltest
directory and typedocker-compose up
. When the terminal stops writing, you should be able to try the IDE, see next section.
Note
You may need to use sudo
to execute docker-compose
.
Try the IDE¶
Point your browser to http://localhost:30000. You should see the Rell IDE.
Click Select Module (first element in the top bar) and then Create. A new window will appear. Write any name for your module, such as Hello.
In the dropdown, select “Rell”, and select the “Use template for code and tests” option.
This will open a modal element where you can specify the name of the Module and the Language used (in this example: Rell). For convenience you can include template and test code.
Click the blue “Create” button. The screen will now be filled with code.
- Browser
- To the left bar is the Browser. You can use it to work with several examples.
- Editor
- Inside the central element on the left the editor filled with a template of source code.
- Test window
- Inside the central element on the right there is a window with XML. This is a way to run your code, simulating a real application.
- Buttons
- On top of the Editor there is a button “Start Node”, don’t press that one yet. There is also a button “Run tests”. Click it to see that the test passes.
Hello World!¶
As a minimal first application, you can make a Hello World example with a focus on Ukraine.
If you checked the use template box and look at editor section on the top, you will see this code as a template:
class city { key name; }
operation insert_city (name) {
create city (name);
}
This is a small registry of cities. In order to run the code we need a test in XML.
<test>
<block>
<transaction>
<signers><param type="bytea" key="Alice"/></signers>
<operations>
<operation name="insert_city">
<string>Kiev</string>
</operation>
</operations>
</transaction>
</block>
</test>
Click the ‘Run tests’ button, and a green message will appear.
After all this work, we suggest that you put “Relational Blockchain” on your CV.
Next step is to learn more about Rell in the Tutorial.
However if you feel eager to click the Start Node button and create a complete running application, you can learn how to use the javascript client library in the Client section.
Videos¶
If you are stuck, we have some videos showing how to get started, on a Mac.
Tutorial¶
Language overview¶
Rell is a language for relational blockchain programming. It combines the following features:
- Relational data modeling and queries similar to SQL. People familiar with SQL should feel at home once they learn the new syntax.
- Normal programming constructs: variables, loops, functions, collections, etc.
- Constructs which specifically target application backends and, in particular, blockchain-style programming including request routing, authorization, etc.
Rell aims to make programming as ergonomic as possible. It minimizes boilerplate and repetition. At the same time, as a static type system it can detect and prevent many kinds of defects.
Blockchain programming¶
There are many different styles of blockchain programming. In the context of Rell, we see blockchain as a method for secure synchronization of databases on nodes of the system. Thus Rell is very database-centric.
Programming in Rell is pretty much identical to programming application backends: you need to handle requests to modify the data in the database and other requests which retrieve data from a database. Handling these two types of requests is basically all that a backend does.
But, of course, before you implement request handlers, you need to describe your data model first.
Class definitions¶
In SQL, usually you define your data model using CREATE TABLE
syntax. In Java, you can define data objects using class
definition.
(See the section about Classes in the language
specification for more information).
Rell uses persistent objects, thus a class definition automatically creates the storage (e.g. a table) necessary to persist objects of a class. As you might expect, Rell’s class definition includes a list of attributes:
class user {
pubkey: pubkey;
name: text;
age: integer;
}
It is very common that the name of the attribute is the same as its
type. For example, it makes sense to call user’s pubkey “pubkey.” Rell
allows you to shorten pubkey: pubkey;
to just pubkey;
. Rell also
has a number of convenient semantic types (see here: Types),
so there is a type called name
as well. Thus you can rewrite the definition above as just:
class user { pubkey; name; }
Typically a system should not allow different users to have the same name.
That is, names should be unique. If name is unique, it can be used to
identify a user. In Rell, this can be done by defining a key, i.e.
key name;
. Note that it’s not necessary to define both key and
attribute. Rell is smart enough to figure out that if you use an
attribute in a key, that attribute should exist in a class.
It also might be useful to find a user by his pubkey. Should it also be
unique? Not necessarily. A user might have several different identities.
When you want to enable fast retrieval, but do not need uniqueness, you
can use index
definition:
class user {
key name;
index pubkey;
}
However, if you want pubkey to be unique for an user, you can add a second key:
class user {
key name;
key pubkey;
}
(See Keys, indices for more information.)
Typically, when you define a class in a programming language, it creates
a type which can be used to refer to instances of that class. This is
exactly how it works in Rell. The definition of class user
creates a
type user
which is a type of references to objects stored in a
database. References can themselves be used as attributes. For example,
you might want to define something owned by a user, say, a channel. You
can describe it like this:
class channel {
index owner: user;
key name;
}
index
makes it possible to efficiently find all channels owned by a
user. key
makes sure that channel names are unique within the
system.
Let’s analyze channel
class definition from a point of view of a traditional relational database terminology.
A single user can be associated with multiple channel
objects, but a single channel
is always related to a single user
.
Thus this represents one-to-many relationship. owner
attribute of a channel refers to user
object and thus constitutes a foreign key.
If channel names should be unique only in context of a single user (e.g.
alice/news
and bob/news
are different channels), then a
composite key can be used:
class channel {
key owner: user, name;
}
This basically means that a pair of (owner, name)
should be unique.
Finally, one might ask: what changes if we change index owner: user
to key owner: user
? This makes a user reference unique per
channel
table, thus there can be at most one channel per user in
that case. (I.e. if owner
is declared as a key, relationship between users and channels becomes a one-to-one relationship.)
Operations¶
Now that we defined the data model, we can finally get to handling requests. As previously mentioned, Rell works with two types of requests:
- Data-modifying requests. We call them
operations
which are applied to the database state. - Data-retrieving requests. We call them
queries
.
But for both types of requests we are going to need to refer to things in the database, so let’s consider relational operators first.
Relational operator basics¶
First, let’s look how we create objects:
create user (pubkey=x"0373599a61cc6b3bc02a78c34313e1737ae9cfd56b9bb24360b437d469efdf3b15",
name="Alice");
This is essentially the same as INSERT
operation in SQL, but the
syntax is a bit different. Rell is smart enough to identify the
connection between arguments and attributes based on their type.
x"..."
notation is a hexadecimal byte_array
literal which is
compatible with pubkey
type. On the other hand, name is provided via
text
literal. Thus we can write:
create user("Alice", x"0373599a61cc6b3bc02a78c34313e1737ae9cfd56b9bb24360b437d469efdf3b15");
The order of arguments does not matter here, they are matched with attributes based on types.
How do we find that object now?
val alice = user @ {.name=="Alice"};
@
operator retrieves a single record (or an object in this case)
satisfying the search criteria you provided. If there is no such record,
or more than one exists, it raises an error. It’s recommended to use this
construct when an operation needs a single record to operate on. If this requirement
is violated the operation will be aborted and all its effects will be rolled back.
Thus it is a succinct and effective way to deal with requirements.
(val
defines a read-only variable which can later be used in an
expression. A variable defined using var
can be reassigned later.)
If you want to retrieve a list of users, you can use the @*
operator. For
example:
val all_users = user @* {};
This returns a list of all users (since no filter expression was provided,
all users match it). Value declarations can include a type,
for example, we can specify that all_users
is of type list<user>
like this:
val all_users: list<user> = user @* {};
Since the Rell compiler knows a type of every expression it does not really need a type declaration, however, if one is provided, it will check against it. Type declarations are mostly useful as documentation for programmers reading the code and should be omitted in cases where there is no ambiguity.
Both @
and @*
correspond to SELECT
in SQL. A complete list of operators
can be found in Operators and delimiters.
Simple operation¶
Let’s make an operation which allows a user to create a new channel:
operation register_channel (user_pubkey: pubkey, channel_name: name) {
require( is_signer(user_pubkey) );
create channel (
owner = user@{.pubkey == user_pubkey},
name = channel_name
);
}
Let’s go through this line by line. First we declare the operation name and a list of parameters:
operation register_channel (user_pubkey: pubkey, channel_name: name) {
This is very similar to a function definitions in other languages. In fact,
an operation is a function of a special kind: it can be invoked using a blockchain
transaction by its name. When invoking register_channel
, the caller must
provide two arguments of specified types, otherwise it will fail.
require( is_signer(user_pubkey) );
We don’t want Alice to be able to pull a prank on Bob by registering a
channel with a silly name on his behalf. Thus we need to make sure that
the transaction was signed with a key corresponding to the public key
specified in the first parameter. (In other words, if Bob’s public key
is passed as user_pubkey
, the transaction must also be signed by Bob,
that is, Bob is a signer of this transaction.)
This is a common pattern in Rell – typically
you specify an actor in a parameter of an operation and in the body of
the operation you verify that the actor was actually the signer.
require
fails the operation if the specified condition is not met.
Note
What happens with a failed operation? A transaction which contains this operation will be rejected, that is, it won’t be included into a blockchain and will be eventually forgotten. When developer is debugging an application he might be able to retrieve a message which describes a reason why a transaction was rejected. However, in a real production use this cannot be relied upon, as blockchain nodes can be bombed with large quantities of invalid transactions, thus nodes normally won’t store a list of rejected transactions.
create channel
, obviously, creates a persistent object channel
.
You don’t need to explicitly store it, as all created objects are
persisted if operation succeeds.
user@{.pubkey=user_pubkey}
– now we retrieve a user object by its
pubkey, which should be unique. If no such user exists operation will
fail. We do not need to test for that explicitly as @
operator will
do this job.
Rell can automatically find attribute names corresponding to arguments
using types. As user
and name
are different types,
create channel
can be written like
this:
create channel (user@{.pubkey=user_pubkey}, channel_name);
Function¶
Sometimes multiple operations (or queries) need a same piece functionality, e.g. some kind of a validation code, or code which retrieves objects in a particular way.
In order to not repeat yourself you can use function
.
Functions work similarly to operations: they get some input and can perform validations and work with data. Additionally, they also have a return type which can be specified after the list of parameters.
For example, if you want to allow the user of a channel to change the name of the channel itself:
// We added mutable specifier to channel's attribute "name" to make name editable.
// Note that in case both an attribute and a key need to be declared.
class channel {
mutable name;
key name;
index owner: user;
}
function get_channel_owned_by_user(user_pub: pubkey, channel_name: name): channel {
val user = user@{.pubkey == user_pub};
return channel@{channel_name, .owner == user};
}
operation change_channel_name(signer: pubkey, old_channel_name: name, new_channel_name: name) {
require(is_signer(signer));
val channel_to_change = get_channel_owned_by_user(signer, old_channel_name);
update channel@{channel == channel_to_change}(.name = new_channel_name);
}
In the function get_channel_owned_by_user
the code first retrieves a user with given public key and returns a channel owned by the the retrieved user with the given channel name. Operator @
expects exactly one object to be found (see Cardinality for more information.), thus you can be sure that in case there is no user or channel with such a pubkey or a name the function will fail and so will the operation that is calling it.
Finally, the function returns the channel instance that was validated, saving the developer the hassle to check owner every time a channel is retrieved.
Please note that you must mark the attribute name
with the keyword mutable
. This is because only the fields which are declared mutable can be changed using the update statement.
Query¶
Storing data without the ability to access it again would be useless. Let’s consider a simple example - retrieving channel names for a user with a certain name:
- ::
- query get_channel_names (user_name: name) {
- return channel @* {
- .owner == user@{.name==user_name}
} (.name);
}
Here you see a selection operator you’re already familiar with –
@*
. We select all the channels with a given owner (which we first
find by name).
Then we extract name attribute from retrieved objects using the
(.name)
construct.
Note that since we only need name
from channel, is also possible to write
- ::
- query get_channel_names (user_name: name) {
- return channel @* {
- .owner == user@{.name==user_name}
}.name;
}
Relational expressions¶
In general, a relational expression consists of five parts, some of which can be omitted:
FROM OPERATOR { WHERE } (WHAT) LIMIT
- FROM describes where data is taken from. It can be a single class,
such as just
user
. Or, it can be combination of multiple classes, e.g.(user, channel)
. In the later case, conceptually we are dealing with a Cartesian product, which is a set of all possible combinations. But, in typically WHERE part will then provide a condition which defines a correspondence between objects of difference classes. E.g. one can select such(user, channel)
combinations whereuser
is an owner of thechannel
. This works same way asJOIN
in SQL, in fact, the optimizer will typically translate it to JOINs. - OPERATOR – there are different operators depending on required
cardinality. They are:
@
– exactly one, returns a value@*
– any number, returns a list of values@+
– at least one, returns a list of values@?
– one or zero, returns a nullable value
- WHERE describes how to filter the FROM set. So, you would use your search criteria as well as JOINs.
- WHAT describes how to process the set, for doing a projection, aggregation or sorting. If it is ommitted then members of the set are returned as they are.
- LIMIT for operators which return a list, limits the number of elements returned.
In SQL, the logical processing order does not match the order in which
clauses are written, for example, FROM
is logically processed before SELECT
even though SELECT
comes first. (SQL logical processing order can be found e.g.
in SQL Server documentation).
The order of components of a relational expression in Rell matches the logical processing order. So, first a set is defined, then it is filtered, and then it is post-processed. Of course, the query planner is allowed to perform operations in a different order, but that shouldn’t affect the results. Thus a relational expression can be understood as a kind of a pipeline.
Let’s see some examples of relational expressions. Suppose in addition
to user
and channel
classes we provided before, we also have:
class message {
index channel;
index timestamp;
text;
}
We can retrieve all messages of a given user:
(channel, message) @* {
channel.owner == given_user, message.channel == channel
}(message.text);
So, basically, we join channel
with message
. We can shorten the
expression using class aliases:
(c: channel, m: message) @* { c.owner == given_user, m.channel == c } (m.text, m.timestamp)
We can easily read this expression left to right:
- consider all pairs
(c, m)
wherec
ischannel
andm
ismessage
- find those where
c.owner
equalsgiven_user
andm.channel
equalsc
- extract
text
andtimestamp
fromm
The result of this expression is a list of tuples with text
and
timestamp
attributes.
The above expression can be easily modified to retrieve the latest 25 messages:
(c: channel, m: message) @* {
c.owner == given_user, m.channel == c
} (m.text, -sort m.timestamp) limit 25
Here we sorted results by timestamp in a descending order using
-sort
(minus prefix means descending) and limited the number of returned rows.
Composite indices¶
We can also only select recent messages by adding, for example,
m.timestamp >= given_timestamp
condition to WHERE part.
But a database cannot filter messages efficiently (that is, without
considering every message) using two criteria at once unless we create
a composite index, changing the message
class definition in the following way:
class message {
index channel, timestamp;
text;
}
Instead two separate indexes we got one composite index. The idea here is that we want to retrieve not the latest messages overall, but the latest messages for a given channel. Thus, we need to order messages by channels first. Paged retieval can be done using the following query:
query get_next_messages (user_name: name, upto_timestamp: timestamp) {
val given_user = user@{user_name};
return (c: channel, m: message) @* {
c.owner == given_user, m.channel == c, m.timestamp < upto_timestamp
} (m.text, -sort m.timestamp) limit 25;
}
This can be used in an app like Twitter. A visitor might first retrieve the latest 25 messages, then go further – in which case the client will send a query with a timestamp of the oldest message retrieved.
To understand why this can work efficiently, consider that the index stores an ordered collection of pairs. For example:
1. (channel_1, 1000000) -> m1
2. (channel_1, 1000050) -> m3
3. (channel_1, 1000100) -> m5
4. (channel_2, 1000025) -> m2
5. (channel_2, 1000075) -> m4
A database can efficiently find a place which corresponds to a given timestamp in a given channel and traverse the index through it.
Note
It’s worth noting that all SQL databases work this way, this feature is not unique to Rell. But in a decentralized system resources are typically precious, thus it is important for Rell programmers to understand the query behavior and use indices efficiently.
Examples and further exercises¶
We have prepared some examples of how to implement other functionality in Rell and Chromia.
General Language Features¶
Types¶
Simple types:¶
boolean
integer
text
byte_array
json
unit
(no value; cannot be used explicitly)null
(type ofnull
expression; cannot be used explicitly)
Simple type aliases:
pubkey
=byte_array
name
=text
timestamp
=integer
tuid
=text
Complex types:¶
- class
T?
- nullable typerecord
- tuple:
(T1, ..., Tn)
list<T>
set<T>
map<K,V>
range
(can be used infor
statement)GTXValue
- used to encode parameters and results of operations and queries
Nullable type¶
- Class attributes cannot be nullable.
- Can be used with almost any type (except nullable,
unit
,null
). - Nullable nullable (
T??
is not allowed). - Normal operations of the underlying type cannot be applied directly.
- Supports
?:
,?.
and!!
operators (like in Kotlin).
Compatibility with other types:
- Can assign a value of type
T
to a variable of typeT?
, but not the other way round. - Can assign
null
to a variable of typeT?
, but not to a variable of typeT
. - Can assign a value of type
(T)
(tuple) to a variable of type(T?)
. - Cannot assign a value of type
list<T>
to a variable of typelist<T?>
.
Allowed operations:
- Null comparison:
x == null
,x != null
. ?:
- Elvis operator:x ?: y
meansx
ifx
is notnull
, otherwisey
.?.
- safe access:x?.y
results inx.y
ifx
is notnull
andnull
otherwise.- Operator
?.
can be used with function calls, e. g.x?.upperCase()
. !!
- null check operator:x!!
returns value ofx
ifx
is notnull
, otherwise throws an exception.require(x)
,requireNotEmpty(x)
: throws an exception ifx
isnull
, otherwise returns value ofx
.
Examples:
val x: integer? = 123;
val y = x; // type of "y" is "integer?"
val z = y!!; // type of "z" is "integer"
val p = require(y); // type of "p" is "integer"
Tuple type¶
Examples:
(integer)
- one value(integer, text)
- two values(integer, (text, boolean))
- nested tuple(x: integer, y: integer)
- named fields (can be accessed asA.x
,A.y
)
Tuple types are compatible only if names and types of fields are the same:
(x:integer, y:integer)
and(a:integer,b:integer)
are not compatible.(x:integer, y:integer)
and(integer,integer)
are not compatible.
Collection types¶
Collection types are:
list<T>
- an ordered listset<T>
- an unordered set, contains no duplicatesmap<K,V>
- a key-value map
Collection types are mutable, elements can be added or removed dynamically.
Only a non-mutable type can be used as a map
key or a set
element.
Following types are mutable:
- Collection types (
list
,set
,map
) - always. - Nullable type - only if the underlying type is mutable.
- Record type - if the record has a mutable field, or a field of a mutable type.
- Tuple - if a type of an element is mutable.
GTXValue¶
GTXValue
is a type used to repsesent encoded arguments and results of remote operation and query calls.
It may be a simple value (integer, string, byte array), an array of values or a string-keyed map.
Some Rell types are not GTX-compatible. Values of such types cannot be converted to/from GTXValue
, and the types
cannot be used as types of operation/query parameters or result.
Rules of GTX-compatibility:
- a
map<K,V>
is GTX-compatible only ifK
istext
. range
is not GTX-compatible- a complex type is not GTX-compatible if a type of its component is not GTX-compatible
For queries, a type must be pretty-GTX-compatible. Rules are:
- a type must be GTX-compatible
- for tuples: either all fields have names, or no field has a name
- component types must be pretty-GTX-compatible as well
Subtypes¶
If type B
is a subtype of type A
, a value of type B
can be
assigned to a variable of type A
(or passed as a parameter of type
A
).
T
is a subtype ofT?
.null
is a subtype ofT?
.(T,P)
is a subtype of(T?,P?)
,(T?,P)
and(T,P?)
.
Module definitions¶
Class¶
class company {
name: text;
address: text;
}
class user {
firstName: text;
lastName: text;
yearOfBirth: integer;
mutable salary: integer;
}
If attribute type is not specified, it will be the same as attribute name:
class user {
name; // built-in type "name"
company; // user-defined type "company" (error if no such type)
}
Attributes may have default values:
class user {
homeCity: text = 'New York';
}
Keys and Indices¶
Classes can have key
and index
clauses:
class user {
name: text;
address: text;
key name;
index address;
}
Keys and indices may have multiple attributes:
class user {
firstName: text;
lastName: text;
key firstName, lastName;
}
Attribute definitions can be combined with key
or index
clauses,
but such definition has restrictions (e. g. cannot specify mutable
):
class user {
key firstName: text, lastName: text;
index address: text;
}
Class annotations¶
class user (log) {
name: text;
}
The log
annotation has following effects:
- Special attribute
transaction
of typetransaction
is added to the class. - When an object is created,
transaction
is set to the result ofop_context.transaction
(current transaction). - Class cannot have mutable attributes.
- Objects cannot be deleted.
Object¶
Object is similar to class, but there can be only one instance of an object:
object event_stats {
mutable event_count: integer = 0;
mutable last_event: text = 'n/a';
}
Reading object attributes:
query get_event_count() = event_stats.event_count;
Modifying an object:
operation process_event(event: text) {
update event_stats ( event_count += 1, last_event = event );
}
Features of objects:
- Like classes, objects are stored in a database.
- Objects are initialized automatically during blockchain initialization.
- Cannot create or delete an object from code.
- Attributes of an object must have default values.
Record¶
Record declaration:
record user {
name: text;
address: text;
mutable balance: integer = 0;
}
- Attributes are immutable by default, and only mutable when declared with
mutable
keyword. - An attribute may have a default value, which is used if the attribute is not specified during construction.
Creating record values:
val u = user(name = 'Bob', address = 'New York');
Same rules as for the create
expression apply: no need to specify attribute name if it can be resolved implicitly
by name or type:
val name = 'Bob';
val address = 'New York';
val u = user(name, address);
val u2 = user(address, name); // Order does not matter - same record object is created.
Record attributes can be accessed using operator .
:
print(u.name, u.address);
Safe-access operator ?.
can be used to read or modify attributes of a nullable record:
val u: user? = findUser('Bob');
u?.balance += 100; // no-op if 'u' is null
Enum¶
Enum declaration:
enum currency {
USD,
EUR,
GBP
}
Values are stored in a database as integers. Each constant has a numeric value equal to its position in the enum (the first value is 0).
Usage:
var c: currency;
c = currency.USD;
Enum-specific functions and properties:
val cs: list<currency> = currency.values() // Returns all values (in the order in which they are declared)
val eur = currency.value('EUR') // Finds enum value by name
val gbp = currency.value(2) // Finds enum value by index
val usdStr: text = currency.USD.name // Returns 'USD'
val usdValue: integer = currency.USD.value // Return 0.
Query¶
- Cannot modify the data in the database (compile-time check).
- Must return a value.
- If return type is not explicitly specified, it is implicitly deducted.
- Parameter types and return type must be pretty-GTX-compatible.
Short form:
query q(x: integer): integer = x * x;
Full form:
query q(x: integer): integer {
return x * x;
}
Operation¶
- Can modify the data in the database.
- Does not return a value.
- Parameter types must be GTX-compatible.
operation createUser(name: text) {
create user(name = name);
}
Function¶
- Can return nothing or a value.
- Can modify the data in the database when called from an operation (run-time check).
- Can be called from queries, operations or functions.
- If return type is not specified explicitly, it is
unit
(no return value).
Short form:
function f(x: integer): integer = x * x;
Full form:
function f(x: integer): integer {
return x * x;
}
When return type is not specified, it is considered unit
:
function f(x: integer) {
print(x);
}
Expressions¶
Values¶
Simple values:
- Null:
null
(type isnull
) - Boolean:
true
,false
- Integer:
123
,0
,-456
- Text:
'Hello'
,"World"
- Byte array:
x'1234'
,x"ABCD"
Text literals may have escape-sequences:
- Standard:
\r
,\n
,\t
,\b
. - Special characters:
\"
,\'
,\\
. - Unicode:
\u003A
.
Tuple:
(1, 2, 3)
- three values(123, 'Hello')
- two values(456,)
- one value (because of the comma)(789)
- not a tuple (no comma)(a = 123, b = 'Hello')
- tuple with named fields
List:
[ 1, 2, 3, 4, 5 ]
Map:
[ 'Bob' : 123, 'Alice' : 456 ]
Operators¶
Special:¶
.
- member access:user.name
,s.sub(5, 10)
()
- function call:print('Hello')
,value.str()
[]
- element access:values[i]
Null handling:¶
?:
- Elvis operator:x ?: y
returnsx
ifx
is notnull
, otherwise returnsy
.?.
- safe access operator:x?.y
returnsx.y
ifx
is notnull
, otherwise returnsnull
; similarly,x?.y()
returns eitherx.y()
ornull
.!!
- null check:x!!
returnsx
ifx
is notnull
, otherwise throws an exception.
Examples:
val x: integer? = 123;
val y = x; // type of "y" is "integer?"
val a = y ?: 456; // type of "a" is "integer"
val b = y ?: null; // type of "b" is "integer?"
val p = y!!; // type of "p" is "integer"
val q = y?.hex(); // type of "q" is "text?"
Comparison:¶
==
!=
===
!==
<
>
<=
>=
Operators ==
and !=
compare values. For complex types (collections, tuples, records) they compare member
values, recursively. For class
object values only object IDs are compared.
Operators ===
and !==
compare references, not values. They can be used only on types:
tuple, record
, list
, set
, map
, GTXValue
, range
.
Example:
val x = [1, 2, 3];
val y = list(x);
print(x == y); // true - values are equal
print(x === y); // false - two different objects
Arithmetical:¶
+
-
*
/
%
Logical:¶
and
or
not
Other:¶
in
- check if an element is in a range/set/map
Statements¶
Local variable declaration¶
Constants:
val x = 123;
val y: text = 'Hello';
Variables:
var x: integer;
var y = 123;
var z: text = 'Hello';
Basic statements¶
Assignment:
x = 123;
values[i] = z;
y += 15;
Function call:
print('Hello');
Return:
return;
return 123;
Block:
{
val x = calc();
print(x);
}
If statement¶
if (x == 5) print('Hello');
if (y == 10) {
print('Hello');
} else {
print('Bye');
}
if (x == 0) {
return 'Zero';
} else if (x == 1) {
return 'One';
} else {
return 'Many';
}
Loop statements¶
For:
for (x in range(10)) {
print(x);
}
for (u in user @* {}) {
print(u.name);
}
The expression after in
may return a range
or a collection
(list
, set
, map
).
While:
while (x < 10) {
print(x);
x = x + 1;
}
Break:
for (u in user @* {}) {
if (u.company == 'Facebook') {
print(u.name);
break;
}
}
while (x < 5) {
if (values[x] == 3) break;
x = x + 1;
}
Database Operations¶
At-Operator¶
Simplest form:
user @ { .name == 'Bob' }
General syntax:
<from> <cardinality> { <where> } [<what>] [limit N]
Cardinality¶
Specifies whether the expression must return one or many objects:
T @? {}
- returnsT
, zero or one, fails if more than one found.T @ {}
- returnsT
, exactly one, fails if zero or more than one found.T @* {}
- returnslist<T>
, zero or more.T @+ {}
- returnslist<T>
, one or more, fails if none found.
From-part¶
Simple (one class):
user @* { .name == 'Bob' }
Complex (one or more classes):
(user, company) @* { user.name == 'Bob' and company.name == 'Microsoft' and user.xyz == company.xyz }
Specifying class aliases:
(u: user) @* { u.name == 'Bob' }
(u: user, c: company) @* { u.name == 'Bob' and c.name == 'Microsoft' and u.xyz == c.xyz }
Where-part¶
Zero or more comma-separated expressions using class attributes, local variables or system functions:
user @* {}
- returns all users
user @ { .name == 'Bill', .company == 'Microsoft' }
- returns a specific
user (all conditions must match)
Attributes of a class can be accessed with a dot, e. g. .name
or with a class name or alias, user.name
.
Class attributes can also be matched implicitly by name or type:
val ms = company @ { .name == 'Microsoft' };
val name = 'Bill';
return user @ { name, ms };
Explanation: the first where-expression is the local variable name
, there is an attribute called name
in the
class user
. The second expression is ms
, there is no such attribute, but the type of the local variable ms
is company
, and there is an attribute of type company
in user
.
What-part¶
Simple example:
user @ { .name == 'Bob' } ( .company.name )
- returns a single value
(name of the user’s company)
user @ { .name == 'Bob' } ( .company.name, .company.address )
- returns
a tuple of two values
Specifying names of result tuple fields:
user @* {} ( x = .company.name, y = .company.address, z = .yearOfBirth )
- returns a tuple with named fields (x
, y
, z
)
Sorting:
user @* {} ( sort .lastName, sort .firstName )
- sort by lastName
first, then by firstName
.
user @* {} ( -sort .yearOfBirth, sort .lastName )
- sort by
yearOfBirth
desdending, then by lastName
ascending.
Field names can be combined with sorting:
user @* {} ( sort x = .lastName, -sort y = .yearOfBirth )
When field names are not specified explicitly, they can be deducted implicitly by attribute name:
val u = user @ { ... } ( .firstName, .lastName, age = 2018 - .yearOfBirth );
print(u.firstName, u.lastName, u.age);
Tail part¶
Limiting records:
user @* { .company == 'Microsoft' } limit 10
Returns at most 10 objects. The limit is applied before the cardinality check, so the following code can’t fail with “more than one object” error:
val u: user = user @ { .company == 'Microsoft' } limit 1;
Result type¶
Depends on the cardinality, from- and what-parts.
- From- and what-parts define the type of a single record,
T
. - Cardinality defines the type of the @-operator result:
T?
,T
orlist<T>
.
Examples:
user @ { ... }
- returnsuser
user @? { ... }
- returnsuser?
user @* { ... }
- returnslist<user>
user @+ { ... }
- returnslist<user>
(user, company) @ { ... }
- returns a tuple(user,company)
(user, company) @* { ... }
- returns `list<(user,company)>``user @ { ... } ( .name )
- returnstext
user @ { ... } ( .firstName, .lastName )
- returns(text,text)
(user, company) @ { ... } ( user.firstName, user.lastName, company )
- returns
(text,text,company)
Nested At-Operators¶
A nested at-operator can be used in any expression inside of another at-operator:
user @* { .company == company @ { .name == 'Microsoft' } } ( ... )
This is equivalent to:
val c = company @ { .name == 'Microsoft' };
user @* { .company == c } ( ... )
Create Statement¶
Must specify all attributes that don’t have default values.
create user(name = 'Bob', company = company @ { .name == 'Amazon' });
No need to specify attribute name if it can be matched by name or type:
val name = 'Bob';
create user(name, company @ { company.name == 'Amazon' });
Can use the created object:
val newCompany = create company(name = 'Amazon');
val newUser = create user(name = 'Bob', newCompany);
print('Created new user:', newUser);
Update Statement¶
Operators @
, @?
, @*
, @+
are used to specify cardinality, like for the at-operator.
If the number of updated records does not match the cardinality, a run-time error occurs.
update user @ { .name == 'Bob' } ( company = 'Microsoft' ); // exactly one
update user @? { .name == 'Bob' } ( deleted = true ); // zero or one
update user @* { .company.name == 'Bad Company' } ( salary -= 1000 ); // any number
Can change only mutable
attributes.
Class attributes can be matched implicitly by name or type:
val company = 'Microsoft';
update user @ { .name == 'Bob' } ( company );
Using multiple classes with aliases. The first class is the one being updated. Other classes can be used in the where-part:
update (u: user, c: company) @ { u.xyz == c.xyz, u.name == 'Bob', c.name == 'Google' } ( city = 'Seattle' );
Can specify an arbitrary expression returning a class, a nullable class or a collection of a class:
val u = user @? { .name == 'Bob' };
update u ( salary += 5000 );
A single attribute of can be modified using a regular assignment syntax:
val u = user @ { .name == 'Bob' };
u.salary += 5000;
Delete Statement¶
Operators @
, @?
, @*
, @+
are used to specify cardinality, like for the at-operator.
If the number of deleted records does not match the cardinality, a run-time error occurs.
delete user @ { .name == 'Bob' }; // exactly one
delete user @? { .name == 'Bob' }; // zero or one
delete user @* { .company.name == 'Bad Company' }; // any number
Using multiple classes. Similar to update
, only the object(s) of the first class will be deleted:
delete (u: user, c: company) @ { u.xyz == c.xyz, u.name == 'Bob', c.name == 'Google' };
Can specify an arbitrary expression returning a class, a nullable class or a collection of a class:
val u = user @? { .name == 'Bob' };
delete u;
Library¶
System classes¶
class block {
block_height: integer;
block_rid: byte_array;
timestamp;
}
class transaction {
tx_rid: byte_array;
tx_hash: byte_array;
tx_data: byte_array;
block;
}
It is not possible to create, modify or delete objects of those classes in code.
chain_context¶
chain_context.raw_config: GTXValue
- blockchain configuration object, e. g. {"gtx":{"rellSrcModule":"foo.rell"}}
chain_context.args: module_args?
- module arguments specified in raw_config
under path gtx.rellModuleArgs
.
The type is module_args
, which must be a user-defined record. If no module_args
record is defined in the module,
the args
field cannot be accessed. The value is null
if arguments are not specified in the module configuration.
Example of module_args
:
record module_args {
s: text;
n: integer;
}
Corresponding module configuration:
{
"gtx": {
"rellSrcModule": "foo.rell",
"rellModuleArgs": {
"s": "Hello",
"n": 123
}
}
}
Code that reads module_args
:
function f() {
print(chain_context.args?.s);
print(chain_context.args?.n);
}
op_context¶
op_context.last_block_time: integer
- the timestamp of the last block, in milliseconds
(like System.currentTimeMillis()
in Java). Returns -1
if there is no last block (the block currently being built
is the first block).
Can be used only in an operation or a function called from an operation, but not in a query.
op_context.transaction: transaction
- the transaction currently being built.
Can be used only in an operation or a function called from an operation, but not in a query.
Functions¶
Global Functions¶
abs(integer): integer
- absolute value
is_signer(byte_array): boolean
- returns true
if a byte array is
in the list of signers of current operation
json(text): json
- parse a JSON
log(...)
- print a message to the log (same usage as print
)
max(integer, integer): integer
- maximum of two values
min(integer, integer): integer
- minimum of two values
print(...)
- print a message to STDOUT:
print()
- prints an empty lineprint('Hello', 123)
- prints"Hello 123"
Require functions¶
For checking a boolean condition:
require(boolean[, text])
- throws an exception if the argument is
false
For checking for null
:
require(T?[, text]): T
- throws an exception if the argument is
null
, otherwise returns the argument
requireNotEmpty(T?[, text]): T
- same as the previous one
For checking for an empty collection:
requireNotEmpty(list<T>[, text]): list<T>
- throws an exception if
the argument is an empty collection, otherwise returns the argument
requireNotEmpty(set<T>[, text]): set<T>
- same as the previous
requireNotEmpty(map<K,V>[, text]): map<K,V>
- same as the previous
When passing a nullable collection to requireNotEmpty
, it throws an
exception if the argument is either null
or an empty collection.
Examples:
val x: integer? = calculate();
val y = require(x, "x is null"); // type of "y" is "integer", not "integer?"
val p: list<integer> = getList();
requireNotEmpty(p, "List is empty");
val q: list<integer>? = tryToGetList();
require(q); // fails if q is null
requireNotEmpty(q); // fails if q is null or an empty list
integer¶
integer.MIN_VALUE
= minimum value (-2^63
)
integer.MAX_VALUE
= maximum value (2^63-1
)
integer(s: text, radix: integer = 10)
- parse a signed
representation, fail if invalid
integer.parseHex(text): integer
- parse an unsigned HEX
representation
.hex(): text
- convert to an unsigned HEX representation
.str(radix: integer = 10)
- convert to a signed string
representation
.signum(): integer
- returns -1
, 0
or 1
depending on the
sign
text¶
.empty(): boolean
.size(): integer
.compareTo(text): integer
- as in Java
.startsWith(text): boolean
.endsWith(text): boolean
.contains(text): boolean
- true
if contains the given substring
.indexOf(text, start: integer = 0): integer
- returns -1
if
substring is not found (as in Java)
.lastIndexOf(text[, start: integer]): integer
- returns -1
if
substring is not found (as in Java)
.sub(start: integer[, end: integer]): text
- get a substring
(start-inclusive, end-exclusive)
.replace(old: text, new: text)
.upperCase(): text
.lowerCase(): text
.split(text): list<text>
- strictly split by a separator (not a
regular expression)
.trim(): text
- remove leading and trailing whitespace
.matches(text): boolean
- true
if matches a regular expression
.encode(): byte_array
- convert to a UTF-8 encoded byte array
.charAt(integer): integer
- get a 16-bit code of a character
.format(...)
- formats a string (as in Java):
'My name is <%s>'.format('Bob')
- returns'My name is <Bob>'
Special operators:
+
: concatenation[]
: character access (returns single-charactertext
)
byte_array¶
byte_array(text)
- create a byte_array
from a HEX string,
e.g.'1234abcd'
byte_array(list<integer>)
- create a byte_array
from a list;
values must be 0 - 255
.empty(): boolean
.size(): integer
.decode(): text
- decode a UTF-8 encoded text
.sub(start: integer[, end: integer]): byte_array
- sub-array
(start-inclusive, end-exclusive)
.toList(): list<integer>
- list of values 0 - 255
Special operators:
+
: concatenation[]
: element access
range¶
range(start: integer = 0, end: integer, step: integer = 1)
-
start-inclusive, end-exclusive (as in Python):
range(10)
- a range from 0 (inclusive) to 10 (exclusive)range(5, 10)
- from 5 to 10range(5, 15, 4)
- from 5 to 15 with step 4, i. e.[5, 9, 13]
range(10, 5, -1)
- produces[10, 9, 8, 7, 6]
range(10, 5, -3)
- produces[10, 7]
Special operators:
in
- returnstrue
if the value is in the range (takingstep
into account)
list¶
list<T>()
- a new empty list
list<T>(list<T>)
- a copy of the given list (list of subtype is
accepted as well)
list<T>(set<T>)
- a copy of the given set (set of subtype is
accepted)
.empty(): boolean
.size(): integer
.contains(T): boolean
.containsAll(list<T>): boolean
.containsAll(set<T>): boolean
.indexOf(T): integer
- returns -1
if element is not found
.sub(start: integer[, end: integer]): list<T>
- returns a sub-list
(start-inclusive, end-exclusive)
.str(): text
- returns e. g. '[1, 2, 3, 4, 5]'
.add(T): boolean
- adds an element to the end, always returns
true
.add(pos: integer, T): boolean
- inserts an element at a position,
always returns true
.addAll(list<T>): boolean
.addAll(set<T>): boolean
.addAll(pos: integer, list<T>): boolean
.addAll(pos: integer, set<T>): boolean
.remove(T): boolean
- removes the first occurrence of the value,
return true
if found
.removeAll(list<T>): boolean
.removeAll(set<T>): boolean
.removeAt(pos: integer): T
- removes an element at a given position
.clear()
Special operators:
[]
- element access (read/modify)in
- returnstrue
if the value is in the list
set¶
set<T>()
- a new empty set
set<T>(set<T>)
- a copy of the given set (set of subtype is accepted
as well)
set<T>(list<T>)
- a copy of the given list (with duplicates removed)
.empty(): boolean
.size(): integer
.contains(T): boolean
.containsAll(list<T>): boolean
.containsAll(set<T>): boolean
.str(): text
- returns e. g. '[1, 2, 3, 4, 5]'
.add(T): boolean
- if the element is not in the set, adds it and
returns true
.addAll(list<T>): boolean
- adds all elements, returns true
if
at least one added
.addAll(set<T>): boolean
- adds all elements, returns true
if at
least one added
.remove(T): boolean
- removes the element, returns true
if found
.removeAll(list<T>): boolean
- returns true
if at least one
removed
.removeAll(set<T>): boolean
- returns true
if at least one
removed
.clear()
Special operators:
in
- returnstrue
if the value is in the set
map<K,V>¶
map<K,V>()
- a new empty map
map<K,V>(map<K,V>)
- a copy of the given map (map of subtypes is
accepted as well)
.empty(): boolean
.size(): integer
.contains(K): boolean
.get(K): V
- get value by key (same as []
)
.str(): text
- returns e. g. '{x=123, y=456}'
.clear()
.put(K, V)
- adds/replaces a key-value pair
.putAll(map<K, V>)
- adds/replaces all key-value pairs from the
given map
.remove(K): V
- removes a key-value pair (fails if the key is not in
the map)
.keys(): set<K>
- returns a copy of keys
.values(): list<V>
- returns a copy of values
Special operators:
[]
- get/set value by keyin
- returnstrue
if a key is in the map
enum¶
Assuming T
is an enum type.
T.values(): list<T>
- returns all values of the enum, in the order of declaration
T.value(text): T
- finds a value by name, throws en exception if not found
T.value(integer): T
- finds a value by index, throws an exception if not found
Enum value properties:
.name: text
- the name of the enum value
.value: integer
- the numeric value (index) associated with the enum value
GTXValue¶
GTXValue.fromJSON(text): GTXValue
- decode a GTXValue
from a JSON string
GTXValue.fromJSON(json): GTXValue
- decode a GTXValue
from a json
value
GTXValue.fromBytes(byte_array): GTXValue
- decode a GTXValue
from a binary-encoded form
.toJSON(): json
- encode in JSON format
.toBytes(): byte_array
- encode in binary format
record¶
Functions available for all record
types:
T.fromBytes(byte_array): T
- decode from a binary-encoded GTXValue
T.fromGTXValue(GTXValue): T
- decode from a GTXValue
T.fromPrettyGTXValue(GTXValue): T
- decode from a pretty-encoded GTXValue
.toBytes(): byte_array
- encode in binary format
.toGTXValue(): GTXValue
- encode to a GTXValue
.toPrettyGTXValue(): GTXValue
- encode to a pretty-encoded GTXValue
Language specification¶
Lexical Rules¶
Whitespaces and comments¶
Whitespaces are like in Java: characters for which java.lang.Character.isWhitespace(c) == true
(e.g. space
, tab
, end-of-line
, etc.).
Comments are like in Java.
- Single-line comment: starts with
//
, ends with end-of-line or end-of-file. - Multiline comment: starts with
/*
, ends with*/
. - Error if there is no
*/
after/*
.
Identifiers¶
Identifiers are like in Java: first character has java.lang.Character.isJavaIdentifierStart(c) == true
, other characters have java.lang.Character.isJavaIdentifierPart(c) == true
. Simple definition: sequence of letters, digits and underscores (_
), first character is not a digit.
Can contain non-English letters. Identifiers are case-sensitive.
Keywords:¶
Keyword is one of the following reserved identifiers:
and
break
class
create
delete
else
false
for
function
if
in
index
key
limit
list
map
mutable
not
null
operation
or
query
return
set
sort
true
update
val
var
while
- A keyword cannot be used as a general identifier, i.e. as a name of a class, function, variable, etc.
- Longest possible keyword/identifier is taken, i.e. string “format” is an identifier “format”, not keyword “for” and identifier “mat”.
Operators and delimiters¶
List of operators and delimiters:
!!
!=
%
%=
(
)
*
*=
+
+=
,
-
-=
.
/
/=
:
;
<
<=
=
==
>
>=
?
?.
?:
@
[
]
{
}
- Longest possible operator/delimiter is taken, i. e. string
<=
is a single operator<=
, not two operators<
and=
.
Integer literals¶
- Decimal:
regex /[0-9]+/
.
Maximum decimal value: 9223372036854775807
(2^63 - 1
). Error if the value is greater.
- Hex:
regex /0x[0-9A-Fa-f]+/
, e.g.0x0
,0xABCD
, etc.
Maximum hex value: 0x7FFFFFFFFFFFFFFF
(2^63 - 1
). Error if the value is greater.
- Cannot have a letter directly after an integer literal, e. g.
1234X
is an error, not two tokens1234
,X
.
String literals¶
- Enclosed in single (‘) or double (“) quotes.
- There is no difference between single-quoted and double-quoted strings, i. e. ‘Hello’ and “Hello” are equal string literals.
- Cannot contain an end-of-line character (0x0A), i. e. closing quote must be on the same line as the open quote.
- Error if there is no closing quote on the same line.
Escape sequences¶
Simple escape sequences:
\b
\t
\r
\n
\"
\'
\\
Unicode escape sequence:
\u1234
\uABCD
\uAbCd
etc. - must have exactly 4 hex digits.
Error if wrong escape sequence is specified (\
character, but not one of valid escape sequences).
Byte array literals¶
- Syntax:
x"..."
orx'...'
, only hex digits (upper or lower case) can be used within quotes.
Examples:
x''
x"123456"
x"DeadBeef"
- Must start with lower-case
x
, not upper-caseX
. - Must contain an even
(2*N)
number of hex digits (because 1 byte = 2 hex digits). - Cannot contain escape sequences or end-of-lines.
Types¶
General¶
A type of an attribute, parameter, variable, etc. can be:
- name of a built-in or user-defined type (Identifier)
- nullable type
- tuple type
- collection type
Built-in types¶
Basic built-in types are:
boolean
byte_array
integer
json
range
text
Built-in type aliases are:
name = text
pubkey = byte_array
timestamp = integer
tuid = text
Type alias A = T
means that entities (attributes, variables, etc.) of type A
will effectively have type T
during compilation.
Special types¶
Special types cannot be used in code explicitly (in attribute declarations, etc.), but they are used by the compiler internally as types of some expressions.
- Special types are:
unit
,null
. - Names of special types cannot be used in code as types. Trying to use “unit” as a type causes an error e. g. “Unknown type name”. “null” is a keyword, so using it as a type is a syntax error.
Nullable type¶
The idea was taken from Kotlin.
Syntax:
NullableType: Type "?"
Examples:
integer?
list<text>?
Error if the underlying type is nullable, e. g. integer??
.
Tuple type¶
Consists of one or more fields. Each field must have a type and may have a name.
Syntax:
TupleType: "(" TupleTypeField ( "," TupleTypeField )* ")"
TupleTypeField: ( Identifier ":" )? Type
Examples:
(integer)
(integer, text)
(x: integer, y: integer)
(p: text, q: byte_array, list<integer>)
Error if same field name is used more than once.
Collection types¶
Collection types are: list, set, map.
Syntax:
"list" "<" Type ">"
"set" "<" Type ">"
"map" "<" Type "," Type ">"
Examples:
list<integer>
set<text>
map<text, byte_array>
Subtypes¶
Purpose: if type B
is a subtype of type A
, a value of type B
can be assigned to a variable of type A
.
T
is subtype ofT
.T
is subtype ofT?
.null
is subtype ofT?
.Tuple type
T1
is subtype of tuple typeT2
if:- the number of fields is the same
- names of corresponding fields are the same (if a field has no name, the other field must have no name)
- type of each field of T1 is a subtype of the type of the corresponding field of T2
Examples:
(integer, text)
is subtype of(integer, text?)
(integer, text?)
is subtype of(integer?, text?)
(integer, text?)
is not subtype of(integer, text)
, becausetext?
is not subtype oftext
(x: integer, y: integer)
is subtype of(x: integer?, y: integer?)
(x: integer, y: integer)
is not subtype of(p: integer, q: integer)
, because field names differ(integer, text)
is not subtype of(x: integer, y: integer)
(x: integer, y: integer)
is not subtype of(integer, text)
Classes¶
Class has a name and zero or more member definitions.
- When a class with name A is defined, A can be used as a type name in the code after the class definition.
- Error if there already is a built-in or user-defined type with same name.
- Class members are: attribute, key, index.
Class syntax¶
ClassDefinition: "class" Identifier "{" ClassMemberDefinition* "}"
ClassMemberDefinition :
AttributeDefinition
KeyDefinition
IndexDefinition
Example:
class user {
name: text;
address: text;
key name;
index address;
}
Attributes¶
Attribute definition may contain a name, type, default value expression and modifiers (e. g. mutable).
Syntax:
AttributeDefinition: "mutable"? FieldDefinition ("=" Expression) ";"
FieldDefinition: Identifier (":" Type)?
- If type is not specified, same type as the attribute name is taken (built-in or user-defined). Error if there is no such type.
- Error if there already is another attribute with same name in the same class.
- If default value expression is specified, the type of the expression must be a subtype of the attribute’s type.
- Expressions specification will be written later. We can use simplest expressions now for testing: integer literal, string literal, true, false, null, etc.
Examples:
name; // same as "name: name;", there is a built-in type "name"
address: text;
mutable age: integer;
mutable status: text = 'Unknown';
Keys, indices¶
Keys and indices consist of one or more fields.
Syntax:
KeyDefinition: "key" FieldDefinition ("," FieldDefinition)* ";"
IndexDefinition: "index" FieldDefinition ("," FieldDefinition)* ";"
Handling of fields¶
- Error if same field name is used more than once within one key/index.
- If there is no attribute with such name, an attribute is added to the class implicitly. The added attribute is not mutable, has no default value.
- If there is an attribute with such name, the key/index field cannot have a type specified.
No error:
key foo: integer;
Error:
foo: integer;
key foo: integer;
Error if there already is a key/index with same set of fields.
Not an error:
index a;
index a, b;
Error:
index a, b;
index b, a;
It does not matter if a key/index is defined before or after an attribute used in it
Code:
x: integer;
key x;
is equivalent to:
key x;
x: integer;
Same for field type restrictions: does not matter whether it is before or after the attribute definition
No error:
key x: integer;
No error:
x: integer;
key x;
Error:
x: integer;
key x: integer; // ERROR
Error:
key x: integer; // ERROR
x: integer;
Operations, Queries, Functions¶
Let’s say that operations, queries and functions are routines. Some rules are common for all routines, while other rules are specific for operations, queries or functions.
Syntax¶
Module : Definition*
Definition : ClassDefinition | RecordDefinition | RoutineDefinition
RoutineDefinition : Operation | Query | Function
ClassDefinition syntax is covered above.
- Each routine has a name.
- Error when defining a routine, and another routine with the same name already exists.
Built-in functions are also taken into account when checking this rule. (The list of built-in functions will be given in a future chapter.)
Operations¶
Operation: “operation” Identifier “(” FormalParams? “)” BlockStatement
FormalParams: FieldDefinition ( “,” FieldDefinition )*
BlockStatement: “{” Statement* “}”
- FieldDefinition syntax is given in chapter 3 (it’s the same as for class fields).
- Statement syntax will be given in a future chapter about statements.
- Return type of an operation is “unit”. Thus, an operation cannot return a value. Return statement cannot have an expression, even if the expression returns unit:
return; // OK
return print('Hello'); // Error, even though print() returns unit.
Examples¶
operation foo(user; value: integer) {
if (value == 0) return;
update account @ { user } ( score += value );
}
Queries¶
Query: query
Identifier (
FormalParams? )
(:
Type)? QueryBody
QueryBody: SimpleBody | ComplexBody
SimpleBody: =
Expression ;
ComplexBody: BlockStatement
Return type¶
- A query has a specific return type and always returns a value.
- If return type is not specified explicitly, it is implicitly deducted from return expressions.
- For simple body: return type is the type of the expression.
- Error if the type of the expression is “unit”.
- For complex body: return type is the common type of types of all expressions used in return statements.
- Error if there is no common type for return expressions types.
- If explicit return type is specified.
- For simple body: error if the type of the expression is not a subtype of the explicit return type.
- For complex body: error if the type of the expression in a return statement is not a subtype of the explicit return type.
- For complex body: error if there is no return statement.
Examples¶
query getUserCount(company) = (user @* { company }).size(); // Returns integer.
query getUserCount(companyName) {
if (companyName == "") return 0;
return (user @* { company.name == companyName }).size();
}
Error: no common return type
query q(x: integer) {
if (x < 0) return 'Hello';
return 123; // Error on this line.
}
Error: actual return type differs from the declared one
query q(): integer = 'Hello';
query q(): integer { return 'Hello'; }
Functions¶
Function: “function” Identifier “(” FormalParams? “)” (“:” Type)? FunctionBody
FunctionBody: SimpleBody | ComplexBody
Return type¶
- If return type is not specified, the return type of the function is “unit”.
*Simple body
- The type of the expression must be a subtype of the return type of the function.
- The type of the expression cannot be “unit”.
Complex body
- If return type is not specified (thus, it is “unit”), return statements must have no expression (i. e. must use “return;”, not “return X;”).
- If return type is specified, type of expressions in return statements must be a subtype of the return type.
- Order of function definitions does not matter, all functions defined in a module are visible everywhere in the module.
This allows recursive and mutually-recursive functions:
function a(x: integer) {
if (x > 0) b(x - 1); // b() is visible here, but it is defined below.
}
function b(x: integer) {
if (x > 0) a(x - 1);
}
Common things for routines¶
- Queries and non-unit functions must always return a value.
- Error if there is no return statement on one of code paths:
function f(x: integer): integer {
print(x);
} // Error: no return statement at all.
function f(x: integer): integer {
if (x > 0) return x * x;
} // Error: no return statement for one of code paths.
function f(x: integer): integer {
if (x > 0) {
return x * x;
} else {
print('invalid argument');
} // Error: no return statement for this branch.
}
More formal rules how to check if there is a return value will be given in the chapter on statements (future).
Client¶
This client tutorial is a continuation on the quickstart “city” example. In this section we illustrate how to send transactions to and retrieve information from a blockchain node running Rell.
Try the example code¶
First of all, we need to add a query to Rell source file:
query is_city_registered(city_name: text): boolean {
return (city @? { city_name }) != null;
}
Clicking ‘Start node’ will start a Postchain node in a single-node mode which is convenient for testing. The node builds blocks when there are transactions, or at least once every 30 seconds. It also has REST API we can interact with to submit transactions and retrieve information.
The client code is written in JavaScript, this example uses the NodeJS environment. postchain-client-example_
can be downloaded using git:
git clone https://bitbucket.org/chromawallet/postchain-client-example.git
To run it, execute:
npm install
node index.js
This will create a transaction, sign it, submit to a node. And once transaction is added to a block, client will perform a query.
Now let’s see how this client code can be implemented:
Install the client¶
We assume you have nodejs
installed. The client library is called postchain-client_
and can be installed from npm.
Create an new directory for your test. Open a terminal in the new directory, initialize npm and install the client.
npm init -y
npm install postchain-client --save
Connect to the node¶
To connect to a Postchain node we need to know its REST API URL and blockchain identifier. DevPreview bundle comes with following defaults:
const pcl = require('postchain-client');
const node_api_url = "http://localhost:7740"; // using default postchain node REST API port
// default blockchain identifier used for testing
const blockchainRID = "78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3";
const rest = pcl.restClient.createRestClient((node_api_url, blockchainRID, 5);
Once we set up the information about the the REST Client connection, we can create the gtxClient connection. This in particular, needs to receive the previous REST connection, the blockchainRID in Buffer format and an array the names of the operations that you want call (at the moment this can be left empty):
const gtx = pcl.gtxClient.createClient(
rest,
Buffer.from(blockchainRID, 'hex'),
[]
);
Now that the connection is set, you can start to create transactions and queries.
Make a transaction (with operations inside)¶
You need to create the transaction client side, sign it with one or more keypairs, send it to the node and wait for it to be included into a block.
First, let’s create the transaction and specify the public key of the person(s) that will sign it.
To create a random user keypair on the go you can use makeKeyPair()
function.
const user = pcl.util.makeKeyPair();
const tx = gtx.newTransaction([user.pubKey]);
Once it is created is possible call as many operations as you want.
tx.addOperation('insert_city', "Tel Aviv");
tx.addOperation('insert_city', "Stockholm");
/* etc */
Now, all is left is to sign and post the transaction
tx.sign(user.privKey, user.pubKey);
tx.postAndWaitConfirmation();
Note: tx.postAndWaitConfirmation()
returns a promise, and thus can be await
-ed.
Query¶
Queries also make use of gtx
client.
gtx.query
accepts as first parameter the name of the query as specified in the module and
then an object with as parameter name the variable name as specified in the query module.
E.g:
function is_city_registered(city_name) {
return gtx.query("is_city_registered", {city_name: city_name});
}
will work with query specified in the Rell file:
query is_city_registered(city_name: text): boolean {
return (city @? { city_name }) != null;
}
Note: gtx.query(queryName, queryObject)
also returns a promise.
Examples¶
Account-based token system¶
Tokens are the bread & butter of blockchains, thus it is useful to demonstrate how a token system can be implemented in Rell. There are roughly two different implementation strategies:
- Account-based tokens which maintain an updateable balance for each account (which can be associated with a key or an address)
- UTXO-based ones (Bitcoin-style) deal with virtual “coins” which are minted and destroyed in transactions
This section details the account-based implementation. For an example of a UTXO based system see UTXO-based token system.
A minimal implementation can look like this:
class balance {
key pubkey;
mutable amount: integer;
}
operation transfer(from_pubkey: pubkey, to_pubkey: pubkey, xfer_amount: integer) {
require( is_signer(from_pubkey) );
require( xfer_amount > 0 );
require( balance@{from_pubkey}.amount >= xfer_amount );
update balance@{from_pubkey} (amount -= xfer_amount);
update balance@{to_pubkey} (amount += xfer_amount);
}
There are a few items which should be highlighted in this code.
First, let’s note that balance@{from_pubkey}.amount
is simply a
shorthand notation for balance@{from_pubkey} (amount)
.
update
relational operator combines a relational expression specifying
objects to update with a form which specifies how to update their attributes.
Attributes are updateable only if they are market as mutable
.
Note
We don’t need to worry about concurrency issues (i.e. that the balance can change after we checked it) because Rell applies operations within a single blockchain sequentially.
But this minimal implementation is not very useful, as there’s no
mechanism for a wallet to identify payments it receives (without somehow
scanning the blockchain, or asking the payer to share the transaction with the recipient).
Other blockchains systems might resort to third-party tools and complex
protocols to handle this (for example, the Electrum Bitcoin wallet connects
to Electrum Servers which perform blockchain indexing). Rell-based
blockchains can just use built-in indexing to keep track of payment
history. For example, by using the additional payment
class. To make things more efficient, we also wrap pubkey into user
class, thus getting:
class user { key pubkey; }
class balance {
key user;
mutable amount: integer;
}
class payment {
index from_user: user;
index to_user: user;
amount: integer;
timestamp;
}
operation transfer(from_pubkey: pubkey, to_pubkey: pubkey, xfer_amount: integer) {
require( is_signer(from_pubkey) );
require( xfer_amount > 0 );
val from_user = user@{from_pubkey};
val to_user = user@{to_pubkey};
require( balance@{from_user}.amount >= xfer_amount );
update balance@{from_user} (amount -= xfer_amount);
update balance@{to_user} (amount += xfer_amount);
create payment (
from_user,
to_user,
amount=xfer_amount,
timestamp=op_context.last_block_time);
}
Note
In create payment (from_user, to_user, ...)
Rell can
figure out matching attributes from names of local variables as they
match exactly. It is often the case that you can use the same name for
the same concept.)
Note
In a future version of Rell it will be possible to
timestamp objects automatically using the log
annotation, with the added
benefit that they are then linked to the corresponding transaction and block.
The example above can be easily extended to support multiple types of tokens. For example:
class asset { key asset_code; }
class balance {
key user, asset;
mutable amount: integer;
}
Here we use a composite key to keep track of the balance for each
(user, asset)
pair.
Chroma Chat¶
In this section we will write the code for a public chat.
Requirements¶
The requirements we set are the following:
- There is one admin with an amount of tokens automatically assigned (say 1000000)
- The admin is the first person that registers themselves on the dapp
- Any registered user can register a new user and transfer some tokens to her, after having burned 100 tokens as a fee
- Users are identified by their public key
- Channels are streams of messages belonging to the same topic (which is specified in the name of the channel, e.g. “showerthoughts”, where you can send messages with the thoughts you had under the shower).
- Registered users can create channels
- When a new channel is created, only the creator is within the group. She can add any existing users. This operation costs 1 token.
Class definition¶
The structure of it will be:
class user { key pubkey; }
class channel {
key name;
admin: user;
}
class channel_member { key channel, member: user; }
class message {
key channel, timestamp;
index posted_by: user;
text;
}
class balance {
key user;
mutable amount: integer;
}
Let’s analyse it:
- User
- As said, user is solely identified by her public key
- Channel
- Channels are identified by the name (which ideally reflects the topic of the channel itself) and the user who created it. Note that two channels cannot have the same name (
key
) and that an user can be admin of multiple channels. - Message
- One message has the text and reference of the user who sent it. Additionally, the channel and timestamp of publication is recorded. Note that
key channel, timestamp
means that only one message can be sent within a channel at given timestamp (but of course several messages on different channels can be recorded at single timestamp). - Balance
- This is kind of self explanatory: one user has an amount of tokens. Tokens can be spent (or more in general transfered), for this reason the field is marked as
mutable
.
Operations¶
Init¶
To initialize the module, we need to have at least one registered user. We don’t want the user to call this function once the admin is set (i.e. we don’t want users to change the admin). To prevent such event, we create an operation called init
which verified that no users are registered and, in case of positive response, creates a new admin.
operation init (founder_pubkey: pubkey) {
require( (user@*{} limit 1).len() == 0 );
val founder = create user (founder_pubkey);
create balance (founder, 1000000);
}
The operation receives a public key as input (note that it does not verify that signer of the transaction is the same specified in input field founder_pubkey
, meaning you can specify a different public key).
The interesting point is require( (user@*{} limit 1).len() == 0 );
. Here we retrieve a lists of users with a limit of 1: we get the first user in the table. If there is no user, it will return an empty list. Indeed we check its length and if it’s 0 we can proceed in running the operation since there are no users registered.
In the third and fourth line the founder user is created and 1000000 tokens are given to her.
Decrease balance (Function)¶
For convenience we create a function to decrease a user’s balance. We write it because we don’t want to duplicate our checks and potentially create bugs.
function decrease_balance (user, deduct_amount: integer) {
require( balance@{user}.amount >= deduct_amount);
update balance@{user} (amount -= deduct_amount);
}
Register a new user¶
As, said, registered users should be allowed to add new users, with a fee of 100 tokens as specified in val registration_cost = 100
. We then verify that the signer exists, decrease their balance, create the new user and transfer to him a certain positive amount of tokens.
operation register_user (
existing_user_pubkey: pubkey,
new_user_pubkey: pubkey,
transfer_amount: integer
) {
require( is_signer(existing_user_pubkey) );
val registration_cost = 100;
val existing_user = user@{existing_user_pubkey};
require( transfer_amount > 0 );
decrease_balance(existing_user, transfer_amount + registration_cost);
val new_user = create user (new_user_pubkey);
create balance (new_user, transfer_amount);
}
Create a new channel¶
Registered users can create new channels. Given the public key and the name of the channel, we simply have to verify that she is actual registered user, receive the fee, create the channel (if it already exists, the create command will fail since the name is a key
) and add that user as chat member.
operation create_channel ( admin_pubkey: pubkey, name) {
require( is_signer(admin_pubkey) );
val admin = user@{admin_pubkey};
decrease_balance(admin, 100);
val channel = create channel (admin, name);
create channel_member (channel, admin);
}
Add user to channel¶
The admin of a channel (the one who created the channel) can add another user after having paid a fee of 1 token.
So we check once again that the signer is the admin_pubkey
specified, we decrease the admin balance of 1 token, and we add a new user to the channel via channel_member
.
operation add_channel_member (admin_pubkey: pubkey, channel_name: name, member_pubkey: pubkey) {
require( is_signer(admin_pubkey) );
val admin_usr = user@{admin_pubkey};
decrease_balance(admin_usr, 1);
val channel = channel@{channel_name, .admin==user@{admin_pubkey}};
create channel_member (channel, member=user@{member_pubkey});
}
Post a new message¶
People in a channel will love to share their opinions. They can do so with the post_message
operation where a signer is_signer(pubkey)
can post a message in the channel val channel = channel@{channel_name};
he is registered into require( channel_member@?{channel, member} );
after the payment of a 1 token fee. Note the 3 input parameter nop is not used. We will see why later in this section.
operation post_message (channel_name: name, pubkey, message: text, nop: byte_array) {
require( is_signer(pubkey) );
val channel = channel@{channel_name};
val member = user@{pubkey};
require( channel_member@?{channel, member} );
decrease_balance(member, 1);
create message (channel, member, text=message, op_context.last_block_time);
}
Queries¶
It is useful to write data into a database in a distributed fashion, although writing would be meaningless without the ability to read.
Query all channels where a user is registered¶
Getting the channels one user is registered into is simple, selecting from channel_member
with the given user’s public key.
query get_channels(user_pubkey: text) {
return channel_member@*{.member==user@{byte_array(user_pubkey)}}.channel.name;
}
Other simple queries¶
Likewise we can get the balance from one user.
query get_balance(user_pubkey: text) {
return balance@{user@{byte_array(user_pubkey)}}.amount;
}
Retrieve the last message written in a chat, for a channel preview for example. Please note the use of limit
in order to optimize the query.
query get_last_message(channel_name: name) {
return message@?{channel@{channel_name} } (text = .text, posted_by = .posted_by.pubkey, -sort timestamp = .timestamp) limit 1;
}
And the messages sent in one channel sorted from the newest to the oldest.
query get_last_messages(channel_name: name) {
return message@*{ channel@{channel_name} }
( text = .text, posted_by = .posted_by.pubkey, -sort timestamp = .timestamp );
}
Run it¶
Assuming we have the docker-compose.yml
file and we brought it up, we can simply:
- Browse to
localhost:30000
- Create a new module
- Paste the above code in the
code
section (You can find the full code here). - Remove all the tests
<test>
<block>
</block>
</test>
- Click
Run tests
- When the tests are passed, click on
Run Node
Congratulations! You should now have a running node.
Client side¶
At this stage we should have a running node with your freshly made module.
What about interface it with a classy JS based application?
Well to do it we need the client package, called postchain-client
const pcl = require('postchain-client);
const crypto = require('crypto');
Then we need to declare the address of the REST server (which is ran by the node, default is 7740
) and the blockchainRID of the blockchain (in the dev-preview, this is already set to 78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3
and the number of sockets (5).
We then get an istance of GTX Client, via gtxClient.createClient
and giving the rest object and blockchainRID in input. Last parameters is an empty list of operation (this is needed if you don’t use Rell language, in fact, you can also code a module with standard SQL or as a proper kotlin/java module).
const rest = pcl.restClient.createRestClient("http://localhost:7740/", '78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3', 5)
const gtx = pcl.gtxClient.createClient(
rest,
Buffer.from(
'78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3',
'hex'
),
[]
);
Create and send a transaction with the init operation¶
First thing we probably want is to register and create the admin, we do so calling the init
function.
function init(adminPUB, adminPRIV) {
const rq = gtx.newTransaction([adminPUB]);
rq.addOperation('init', adminPUB);
rq.sign(adminPRIV, adminPUB);
return rq.postAndWaitConfirmation();
}
The first thing we do is to declare a new transaction and that it will be signed by admin private key (we provide the public key, so the node can verify the veracity of transaction.
We add the operation called init
and we pass as input argument the admin public key. We then sign the transaction with the private key (we specify the public key in order to correlate which private key refers to which public key in case of multiple signatures).
Finally we send the transaction to the node via the method postAndWaitconfirmation
which returns a promise and resolves once it is confirmed.
Given the following keypair, we can create the admin.
const adminPUB = Buffer.from(
'031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f',
'hex'
);
const adminPRIV = Buffer.from(
'0101010101010101010101010101010101010101010101010101010101010101',
'hex'
);
init(adminPUB, adminPRIV);
Create other operations¶
We can also create a new channel, post a message, invite a user to dapp, invite a user in a channel
function createChannel(admin, channelName) {
const pubKey = pcl.util.toBuffer(admin.pubKey);
const privKey = pcl.util.toBuffer(admin.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("create_channel", pubKey, channelName);
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();
}
function postMessage(user, channelName, message) {
const pubKey = pcl.util.toBuffer(user.pubKey);
const privKey = pcl.util.toBuffer(user.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("post_message", channelName, pubKey, message, crypto.randomBytes(32));
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();
}
function inviteUser(existingUser, newUserPubKey, startAmount) {
const pubKey = pcl.util.toBuffer(existingUser.pubKey);
const privKey = pcl.util.toBuffer(existingUser.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("register_user", pubKey, pcl.util.toBuffer(newUserPubKey), parseInt(startAmount));
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();
}
function inviteUserToChat(existingUser, channel, newUserPubKey) {
const pubKey = pcl.util.toBuffer(existingUser.pubKey);
const privKey = pcl.util.toBuffer(existingUser.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("add_channel_member", pubKey, channel, pcl.util.toBuffer(newUserPubKey));
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();
}
Although there is really nothing critical in these functions, there are few things worth noting:
- We expect public and private keys in
hex
format, and we convert them to Buffer withpcl.util.toBuffer(admin.pubKey);
- In order to protect the system from replay attacks, the blockchain does not accept transactions which hash is equal to an already existing transaction. This means that an user is not allowed to write the same message twice in a channel since if at day one he writes “hello” the transaction will be something like
rq.addOperation("post_message", the_channel, user_pub, "hello");
, when he will write ‘hello’ a second time the transaction will be the same and therefore rejected. To solve this problem we add some random bytes viacrypto.randomBytes(32)
, and create a different transaction hash.
Querying the blockchain from the client side¶
Previously we wrote the queries on blockchain side. Now we need to query from the dapp. To do so we use the previously mentioned postchain-client
package.
// Rell query, reported here for easy look up
// query get_balance(user_pubkey: text) {
// return balance@{user@{byte_array(user_pubkey)}}.amount;
//}
function getBalance(user) {
return gtx.query("get_balance", {
user_pubkey: user.pubKey
});
}
As you can see everything is contained into gtx.query
: the first argument is the query name in the rell module, and the second argument is the name of the expected attribute in the query itself wrapped in an object. The name of the object is the one specified in module and the value, of course, the value we want to send. Please note that buffer
values must before be converted into hexadecimal strings.
Other queries:
function getChannels(user) {
return gtx.query("get_channels", {
user_pubkey: user.pubKey
});
}
function getMessages(channel) {
return gtx.query("get_last_messages", {channel_name: channel});
}
function getLastMessage(channelName) {
return gtx.query("get_last_message", {
channel_name: channelName
});
}
UTXO-based token system¶
As an exercise, we can also implement a Bitcoin-style token system.
We first define an unspent transaction output structure:
class utxo {
pubkey;
amount: integer;
}
Then define the transfer operation that roughly follows Bitcoin transaction structure – it has a list of inputs and outputs:
operation transfer (inputs: list<utxo>, output_pubkeys: list<pubkey>, output_amounts: list<integer>) {
var input_sum = 0;
for (an_utxo in inputs) {
require(is_signer(an_utxo.pubkey));
input_sum += an_utxo.amount;
delete utxo@{utxo == an_utxo};
}
var output_sum = 0;
require(output_pubkeys.size() == output_amounts.size());
for (out_index in range(output_pubkeys.size())) {
output_sum += output_amounts[out_index];
create utxo (output_pubkeys[out_index],
output_amounts[out_index]);
}
require(output_sum <= input_sum);
}
There are quite a lot of new constructs used in this example:
list<...>
is, obviously, a collection. Besides lists, Rell also supportsset
andmap
, see Collection types for syntax.- in
list<utxo>
utxo object references are physically implemented using integer identifiers which are used interally an_utxo.pubkey
accesses an attribute of an object, which is a database query identical toutxo@{utxo==an_utxo} (pubkey)
- variable type is automatically inferred from expression used for
initialization. One can also write it like
var output_sum : integer = 0;
delete
operation accepts a relational expression which identifies object(s).size()
method can be used get the size of a collectionfor (... in ...)
works both for collections and for ranges of integer values[]
is used to refer to an element of a collection
Note that we perform checks as we go. This is OK because Rell is transactional: if a requirement fails or an error is generated, the whole operation (in fact, the whole transaction) is rolled back. Rell is typically used with a GTX transaction format which supports multiple signers and multiple operations per transaction. Thus it can easily support Bitcoin-style multi-input transactions, atomic token swaps, multi-sig etc.
Now a bit about delete
operator. Isn’t it strange to enable deletion
of data from a blockchain?!
Here we aren’t deleting data “from a blockchain”, we are removing entries from the current blockchain state. This is exactly how it works in a Bitcoin node – once entries in an unspent transaction output set are spent, they are deleted. A typical Bitcoin node doesn’t keep track of spent transaction outputs.
A system based on Rell (e.g. Postchain or Chromia) works in exactly the same way: raw information about transactions and operations is preserved in a blockchain. The database contains both raw blockchain transactions and processed current state. The current state is what a Rell programmer can work with: he is allowed to do destructive updates and delete entries. These operations do not affect the raw blockchain.