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:

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

operation transfer(from_pubkey: pubkey, to_pubkey: pubkey, xfer_amount: integer) {
          require( op_context.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. We will be using the log annotation to add the transaction as an attribute to the entity. This can be used to timestamp the transaction, more about the log annotation and the transsaction entity can be found under the system library chapter. To make things more efficient, we also wrap pubkey into user class, thus getting:

entity user { key pubkey; }

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

@log entity payment {
      index from_user: user;
      index to_user: user;
      amount: integer;
      timestamp;
}

operation transfer(from_pubkey: pubkey, to_pubkey: pubkey, xfer_amount: integer) {
          require( op_context.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);
}

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

The example above can be easily extended to support multiple types of tokens. For example:

entity asset { key asset_code; }

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

Here we use a composite key to keep track of the balance for each (user, asset) pair.

Client Side API

Lets see how we would call this transfer in the front-end. First of all, we need to initialize a user with some starting money. We also need a way to add more users to the network. So before we start writing the front-end we add an init function to our Rell module and also a register user function.

The init function:

operation init (founder_pubkey: pubkey) {

   require( (user@*{} limit 1).size() == 0 );
   val founder = create user(founder_pubkey);
   create balance (founder, 1000000);
}

The register user function:

operation register_user (
   existing_user_pubkey: byte_array,
   new_user_pubkey: byte_array
   ) {
   require( op_context.is_signer(existing_user_pubkey) );
   val existing_user = user@{existing_user_pubkey};
   val new_user = create user (new_user_pubkey);
   create balance (new_user, 0);
   }

Now you can start writing a front-end in nodeJS. If you need a refresher on the installation, check out the “Client Side” chapter in Rell basics. We start by adding the postchain package and start an instance of a GTX client.

const pcl = require('postchain-client');
const nodeApiUrl = "https://rellide-staging.chromia.dev/node/XXXXX/";  //Fill this url with where your node is.
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'
    ),
    []
);

Now we have all we need to start sending transactions to our backend. We start by defining a function that sends a transaction with the init operation inside.

async function initialize(admin){
        const adminPubKey = pcl.util.toBuffer(admin.pubKey);
        const tx = gtx.newTransaction([admin.pubKey]);
        tx.addOperation("init", adminPubKey);
        tx.sign(admin.privKey, admin.pubKey);
        await tx.postAndWaitConfirmation();
}

Now we can write out the function for registering a new user:

async function registerUser(newUser, oldUser){
        const newUserPubKey = pcl.util.toBuffer(newUser.pubKey);
        const oldUserPubKey = pcl.util.toBuffer(oldUser.pubKey);
        const oldUserPrivKey = pcl.util.toBuffer(oldUser.privKey);
        const tx = gtx.newTransaction([oldUserPubKey]);
        tx.addOperation("register_user", oldUserPubKey, newUserPubKey);
        tx.sign(oldUserPrivKey, oldUserPubKey);
        await tx.postAndWaitConfirmation();
}

And lastly, the transfer function:

async function transferBalance(fromUser, toUser, amount) {
        const fromUserPubKey = pcl.util.toBuffer(fromUser.pubKey);
        const fromUserPrivKey = pcl.util.toBuffer(fromUser.privKey);
        const toUserPubKey = pcl.util.toBuffer(toUser.pubKey);
        const tx = gtx.newTransaction([fromUserPubKey]);
        tx.addOperation("transfer_balance", fromUserPubKey, toUserPubKey, amount);
        tx.sign(fromUserPrivKey, fromUserPubKey);
        await tx.postAndWaitConfirmation();
}