Exploring Tokenized Assets on Hedera Consensus Service: part 3
May 16, 2020
by Tim McHale and Greg Scullard
Developer Advocates

This blog series was jointly written by Tim McHale and Greg Scullard (Developer Advocates).

This is part 3 of a 4-part series. In part 2, we introduced and proposed an application network architecture for managing tokens.

From here on, it’s all code! Please note, this blog isn’t a step by step code example for you to follow and build, rather a detailed explanation of the code behind the example. Please consult the README.md for the example to install it and run it.

You can find all the source code and the README.md for this blog in the HCSToken example.

The primitives supported by the example are:

  • Construct
  • Mint
  • Transfer
  • BalanceOf
  • TotalSupply
  • Name
  • Decimals
  • Symbol

We have not implemented Approve and TransferFrom at the time of writing this blog, but it may well be done by the time you read it.

We are dissecting Construct and BalanceOf in this blog so that you get a good understanding of a function call that results in a state change (construct) and one that merely consults local state (BalanceOf).

State model

There are a few ERC-20 token models out there (or iterations thereof), we’ve chosen the OpenZepplin implementation for this example.

contract ERC20 is Context, IERC20 {
using SafeMath for uint256;
using Address for address;

mapping (address => uint256) private _balances;

mapping (address => mapping (address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;
uint8 private _decimals;

Translating this to Java in our example we have two classes:

/**

* This class holds the token and its properties

* Some properties such as totalSupply are ERC20 related

* Others such as the AddressBook, topicId and last consensus are related to AppNet or instance of the App

*/

public final class Token {

// ERC20 properties

private long totalSupply = 0;

private String symbol = "";

private String name = "";

private int decimals = 0;

private Map<String, Address> addresses= new HashMap<String, Address>();

This uses Token.java.

These properties match those found in the Solidity implementation, we’ve added a few more below for the purpose of the application.

// AppNet specifics

private long lastConsensusSeconds = 0;

private int lastConsensusNanos = 0;

private String topicId = "";

// Getters and Setters

We’re holding the Topic Id in the model so that subsequent executions of the application code don’t require the Topic Id to be supplied.

We are also holding the consensus timestamp of the last notification from mirror node. This enables us to subscribe to mirror node for only new consensus transactions on application restart.

Finally, we need a class to hold data related to an address, namely the public key by which the address is referenced, its balance and whether this address is the owner of the token.

/**

* An Address represents a token holder with a balance

* An address also holds a boolean indicating if the address is the owner of the token

* This could be used to validate operations such as Burn for example

*/

Public final class Address {

private long balance = 0;

private String publicKey = "";

private boolean owner = false;

// Getters and Setters

This uses Address.java.

So far so good, now let’s look at user interaction.

User interaction

A user of the decentralized application (a token holder in this example) could technically be without a Hedera account, the node operator would use their own account to submit transactions to Hedera. Alternatively, with a Hedera account, a user could send transactions to HCS themselves.

In this example, the operator of the application node is also the user and it is assumed that the user has a Hedera account along with the private key associated to the account’s public key. This is specified in the .env file of the project.

This approach enables us to more closely simulate the behaviour of a token implementation on Ethereum where the originator of a transaction is an Ethereum wallet holder.

All user interaction is managed through the HCSToken class.

Let’s take a closer look.

public final class HCSToken

{

public static void main( String[] args ) throws Exception {

// create or load state

Token token = Persistence.loadToken();

With Persistence.loadToken(); we recover the current state of the token from a json file. Indeed, a decentralized application has to manage its own storage; this is not provided by the distributed ledger anymore. We could have used a database here, but wanted to keep the example simple.

if (args.length == 0) {

System.out.println("Missing command line arguments, valid commands are (note, not case sensitive)");

System.out.println(" construct {name} {symbol} {decimals}");

System.out.println(" transfer {address} {quantity}");

System.out.println(" mint {quantity}");

System.out.println(" balanceOf {address}");

System.out.println(" totalSupply");

System.out.println(" name");

System.out.println(" decimals");

System.out.println(" symbol");

System.out.println();

System.out.println(" join {topicId}");

System.out.println(" genkey - generates a key pair");

System.out.println(" refresh - pulls updates from mirror node");

System.out.println("Exiting");

return;

}

We then have some simple input validation and help printout in the event no parameters are given.

System.out.print("--> ");

for (String argument : args) {

System.out.print(String.format(" %s", argument));

}

System.out.println();

Just printing the input parameters to console here, nothing to see.

switch (args[0].toUpperCase()) {



Followed by a switch statement to deal with the various possible commands such as construct, mint, etc…

We’ll focus on balanceOf (a query) and construct (a function), the remaining operations are identical in principle.

Going back to Solidity, balanceOf is expressed as follows:

function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}



All this function does is return the value of the _balances array for the given account (or address).

Converting this to Java, we get the following:

case "BALANCEOF":

// balanceOf {address}

Address address = token.getAddress(args[1]);

if (address == null) {

System.out.println("BalanceOf " + args[1] + " : 0");

} else {

System.out.println("BalanceOf " + args[1] + " : " + address.getBalance());

}

break;

Address address = token.getAddress(args[1]) finds the address object from the token for the given address string.

If the address is not found, we return a 0 balance, else we return address.getBalance().

There was no need here to send a transaction to HCS, the local state contains the balance of all the addresses so we can simply query our local state and return the appropriate balance.

Consensus was used at some point to reach agreement between the application network nodes that a balance should be updated, from then on, it’s just a matter of querying that updated balance value.

Now let’s take a look at an example that requires consensus. Bear in mind that we are capturing a user’s intent to construct a Token here, just like a node in a DLT would receive a transaction to instantiate a contract, it would have to be submitted for consensus before the contract can be instantiated.

// primitives

case "CONSTRUCT":

// construct {name} {symbol} {decimals}

Transactions.construct(token, args[1], args[2], parseInt(args[3]));

break;

The construct command invokes a method from the Transactions class.

The next section details how the Transactions class is used to generate and send messages to HCS. Before we jump ahead, let’s take a look at the last line of the HCSToken class.

// save state

Persistence.saveToken(token);

Persistence.saveToken(token); stores the current state of the token object to a local file before the program exits.

Sending messages

Sending messages to HCS for consensus is done with the Transactions class.

First, we initialise some variables with data from environment variables (or .env file).

public final class Transactions {

private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));

private static final Ed25519PrivateKey OPERATOR_KEY = Ed25519PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));

private static final Client client = Client.forTestnet();

The first method in the class is construct which takes the current token state (it would be empty at this stage), the name, symbol and decimals for the new token.

/**

* Constructs a token (similar to an ERC20 token construct function)

*

* @param token: The token object

* @param name: The name of the token

* @param symbol: The symbol for the token

* @param decimals: The number of decimals for this token

* @throws Exception

*/

public static void construct(Token token, String name, String symbol, int decimals) throws Exception {

We start by checking a token doesn’t already exist in local state, if it doesn’t we initialise the SDK client, else we print an error message.

if (token.getTopicId().isEmpty()) {

client.setOperator(OPERATOR_ID, OPERATOR_KEY);

We follow this with the creation of a new ConsensusTopicId for our token, all consensus related to this token will now take place on this Topic Id.

TransactionId transactionId = new ConsensusTopicCreateTransaction()

.execute(client);

final ConsensusTopicId topicId = transactionId.getReceipt(client).getConsensusTopicId();

System.out.println("New topic created: " + topicId);

token.setTopicId(topicId.toString());

We are now set, we can create a HCS transaction to broadcast the creation request on the topic and request consensus on this request from Hedera.

The first stage is to create a construct message from the protobuf definition.

Here we are using two messages, construct and Primitive.

message Construct {

string name = 1;
string symbol = 2;
int32 decimals = 3;
}

Construct is the message conveying the parameters for the token construction or creation.

message Primitive {
bytes signature = 1;
string publicKey = 2;

oneof primitive {
Construct construct = 3;
Mint mint = 4;
Transfer transfer = 5;
Join join = 6;
}
}

Primitive is a wrapper for all types of operations, which also specifies signature and publicKey which is required on all messages. These two parameters enable any application network node to validate the originator of a transaction. If the public key isn’t in our list of addresses, we know we have an imposter and anyone pretending to be a valid user would not be able to sign the message with the corresponding private key, so application network nodes are again able to foil the attack.

Ok, so let’s construct this construct message with the token parameters

Construct construct = Construct.newBuilder()

.setName(name)

.setSymbol(symbol)

.setDecimals(decimals)

.build();

Generate a signature for this construct message using the private key we got from the environment

byte[] signature = OPERATOR_KEY.sign(construct.toByteArray());


Primitive primitive = Primitive.newBuilder()

.setConstruct(construct)

.setSignature(ByteString.copyFrom(signature))

.setPublicKey(OPERATOR_KEY.publicKey.toString())

.build();


HCSSend(token, primitive);

HCSSend is a very simple method which initialises the SDK client, creates a new ConsensusMessageSubmitTransaction on the application network’s topic Id with the Primitive as a message.

It then waits for a receipt containing the status of the transaction.

/**

* Generic method to send a transaction to HCS

*

* @param token: The token object

* @param primitive: The primitive (message) to send

* @throws Exception: in the event of an error

*/

private static void HCSSend(Token token, Primitive primitive) throws Exception

{

client.setOperator(OPERATOR_ID, OPERATOR_KEY);

new ConsensusMessageSubmitTransaction()

.setTopicId(ConsensusTopicId.fromString(token.getTopicId()))

.setMessage(primitive.toByteArray())

.execute(client)

.getReceipt(client);

}

That’s it, we have taken input from a user for a new token and sent that user request to HCS.

All that remains is to subscribe to notifications from a Mirror Node and process these notifications. Let’s finish walking through just that in part 4.