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;
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
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)");
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.
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();
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
HCSSend is a very simple method which initialises the SDK client, creates a new ConsensusMessageSubmitTransaction on the application network’stopic 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
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.