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 them, after having paid 100 tokens to the admin 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.

Entity definition

The structure of it will be:

entity user {
  key pubkey;
  key username: text;
}

entity channel {
  key name;
  admin: user;
}

entity channel_member {
  key channel, member: user;
}

entity message {
  key channel, timestamp;
  index posted_by: user;
  text;
}

entity balance {
  key user;
  mutable amount: integer;
}

Let’s analyse it:

User
As said, user is solely identified by their 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’ user.

operation init (founder_pubkey: pubkey) {
  require( (user@*{} limit 1).size() == 0 );
  val founder = create user (founder_pubkey, "admin");
  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).size() == 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 size() 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 an user with usernam “admin” is created and 1000000 tokens are given to her.

Transfer tokens (Function)

For convenience we create a function to transfer token from one user’s balance to another’s.

We write it because we don’t want to duplicate our checks and potentially create bugs.

function transfer_balance(from:user, to:user, amount:integer){
  require( balance@{from}.amount >= amount);
  update balance@{from} (amount -= amount);
  update balance@{to} (amount += amount);
}

We also add a pay_fee function that is a transfer from one user to the admin account:

function pay_fee (user, deduct_amount: integer) {
  if(user.username != 'admin'){
    transfer_balance(user, user@{.username == 'admin'}, deduct_amount);
  }
}

Register a new user

As said, registered users should be allowed to add new users:

operation register_user (
    existing_user_pubkey: pubkey,
    new_user_pubkey: pubkey,
    new_user_username: text,
    transfer_amount: integer
) {
  require( is_signer(existing_user_pubkey) );
  val existing_user = user@{existing_user_pubkey};

  require( transfer_amount > 0 );

  val new_user = create user (new_user_pubkey, new_user_username);
  pay_fee(existing_user, 100);

  create balance (new_user, 0);
  transfer_balance(existing_user, new_user, transfer_amount);
}

Here we:

  • Verify that the signer exists with user@{existing_user_pubkey}, which require exactly one result for the pubkey.
  • Pay the fee of 100 tokens (transfer 100 tokens to ‘admin’ account)
  • Then create the new user and transfer to them the specified positive amount of tokens.

Note

If at any point in the operation the conditions fail (for example, when the new username is already taken), the whole operation is rolled back and the transaction is rejected.

This is why we don’t need to check if the signer’s balance has registration_cost + transfer_amount tokens beforehand.

Create a new channel

Registered users can create new channels.

Given the public key and the name of the channel, we will verify that she is an actual registered user, transfer the fee, create the channel, and add that user as chat member.

operation create_channel ( admin_pubkey: pubkey, name) {
  require( is_signer(admin_pubkey) );
  val admin_usr = user@{admin_pubkey};
  pay_fee(admin_usr, 100);
  val channel = create channel (admin_usr, name);
  create channel_member (channel, admin_usr);
}

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 have the channel admin pay 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_username: text) {
  require( is_signer(admin_pubkey) );
  val admin_usr = user@{admin_pubkey};
  pay_fee(admin_usr, 1);
  val channel = channel@{channel_name, .admin==user@{admin_pubkey}};
  create channel_member (channel, member=user@{.username == member_username});
}

Post a new message

People in a channel will love to share their opinions. They can do so with the post_message operation. The signer (is_signer(pubkey)) can post a message in the channel (val channel = channel@{channel_name};) if they are a member of the channel (require( channel_member@?{channel, member} );).

After the payment of 1 token fee, we add the new message to the channel:

operation post_message (channel_name: name, pubkey, message: text) {
  require( is_signer(pubkey) );
  val channel = channel@{channel_name};
  val member = user@{pubkey};
  require( channel_member@?{channel, member} );
  pay_fee(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(pubkey):list<(name:text, admin: text)> {
  return channel_member@*{.member == user@{pubkey}} (name = .channel.name, admin = .channel.admin.username);
}

Other simple queries

Likewise we can get the balance from one user.

query get_balance(pubkey) {
  return balance@{ user@{ pubkey } }.amount;
}

Retrieve messages sent in one channel sorted from the oldest to newest (sort .timestamp).

query get_last_messages(channel_name: name):list<(text:text, poster:text, timestamp:timestamp)> {
  return message@*{ channel@{channel_name} }
    ( .text, poster=.posted_by.username, sort .timestamp );
}

Run it

  • Browse to https://rellide-staging.chromia.dev
  • Create a new module
  • Enter the above code in the code section (You can copy the full code from here).
  • Click on Start Node (The green “Play” icon)

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 postchain-client npm package

npm i --save 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 and the number of sockets (5).

We then get an instance 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).

// Check the node log on rellide-staging.chromia.dev to get node api url.
const nodeApiUrl = "https://rellide-staging.chromia.dev/node/XXXXX/";
const blockchainRID = "78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3"; // default RID on rellide-staging.chromia.dev
const rest = pcl.restClient.createRestClient(nodeApiUrl, blockchainRID, 5)
const gtx = pcl.gtxClient.createClient(
    rest,
    Buffer.from(
        blockchainRID,
        'hex'
    ),
    []
);

Note

If you are using Eclipse IDE, the configs should be:

const nodeApiUrl = "http://localhost:7740/";
const blockchainRID = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";

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(adminPubkey, adminPrivkey) {
    const rq = gtx.newTransaction([adminPubkey]);
    rq.addOperation('init', adminPubkey);
    rq.sign(adminPrivkey, adminPubkey);
    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);

Note

In your own project, you might want to generate the keypair using pcl.util.makeKeyPair() instead:

const user = pcl.util.makeKeyPair();
const { pubKey, privKey } = user;

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("nop", crypto.randomBytes(32));
    rq.addOperation("post_message", channelName, pubKey, message);
    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 with pcl.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 a “nop” operation with some random bytes via rq.addOperation("nop", crypto.randomBytes(32));, and create a different transaction hash.

Important

It is very important to remember this limitation imposed upon transactions. If your transaction is rejected with no obvious reason, chances are high that it is missing a “nop” operation.

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 [email protected]{[email protected]{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});
}

Conclusion

At this point, we have created a Rell backend for the public chat, and a javascript client to communicate with it.

We encourage you to extends this sample in anyway you like, for example adding an user interace, or maybe add a “transfer” operation to send tokens to another user?

Or, if you are eager to see how the application running, we have implemented a simple UI for it at https://bitbucket.org/chromawallet/chat-sample/src/master/.