This is a sample implementation and ERC20 like token in a Hedera Consensus Service(HCS) Decentralized Application.
The purpose is to show how to build the necessary components to implement a token on HCS.
- Definition for messages (this is done in protobuf here so that the implementation is easily portable to other languages)
- A model for the state (Token and addresses primarily)
- A means to generate messages to send to HCS
- A means to subscribe to notifications from a mirror node and act upon the state as a result of processing the notifications.
- Ensuring that duplicate messages aren't processed to avoid double spend.
To keep things simple, it is operated with command line inputs and state is held in a json file.
- User runs the application with command line inputs specifying an operation such as
construct
- Inputs are validated and a primitive message specifying an operation such as
construct
,mint
,join
ortransfer
is prepared - The operation is signed by the
OPERATOR_KEY
(private key) found in the.env
file - The signature is added to the primitive message
- The public key of the user (derived from OPERATOR_KEY in
.env
) is added to the primitive message - This primitive message is sent to HCS for consensus
- No changes have been made to state (Except for construct which sets the topicId)
- User runs the application with command line inputs specifying a query such as
balanceOf
- The specified query is run against local state and printed to the console
This is a "special" command which instructs the application to subscribe to a mirror node and act upon the notifications that result from operations above.
When a notification is received
- it is parsed
- the signature is verified
- operations that can only be performed by the owner are rejected if not initiated by the correct address
- inputs are checked against state (e.g. does an address have sufficient balance for a transfer)
- if all successful, local state is updated
The project is built using maven
Linux/MacOs
./mvnw clean install
Windows
mvnw clean install
- copy
.env.sample
to.env
- edit
.env
and set theOPERATOR_ID
andOPERATOR_KEY
Once built, you can try it out with the following commands which will create the token, mint it and then query it.
# construct the token
java -jar hcs-token-example-1.0-run.jar construct TestToken TTT 2
# mint the token
java -jar hcs-token-example-1.0-run.jar mint 1000
# wait for a mirror update
java -jar hcs-token-example-1.0-run.jar refresh
# query information about the token
java -jar hcs-token-example-1.0-run.jar name
java -jar hcs-token-example-1.0-run.jar symbol
java -jar hcs-token-example-1.0-run.jar decimals
java -jar hcs-token-example-1.0-run.jar totalSupply
** Note: Local state is stored in a file called {your operator id}.json
, to reset the environment, simply delete the file **
Any operation that affects state is only applied as a result of a mirror notification, no local state updates are performed unless they are motivated by a mirror notification. This ensures the consistency of state across all instances of the application. Indeed, if a HCS transaction failed for some reason and local state had been updated previously, there would be a discrepancy in state between application instances.
This update should be run before any queries to ensure the local application state is up to date.
java -jar hcs-token-example-1.0-run.jar refresh
Note: A more complete implementation would run this in the background as a thread for example to ensure local state is updated as promptly as possible
Note: Allow a few seconds after construct
to run refresh
to allow the new TopicId
to be propagated to mirror nodes.
construct {name} {symbol} {decimals}
This constructs a HCS transaction to construct the token with a name
, symbol
and decimals
.
It will automatically create a new HCS TopicId
and return it to the console, you can communicate this topic Id to others so they can join your App Net.
This will also be stored in {your operator id}.json
so that it is remembered by the application.
Note: If you include a space in the token name, be sure to surround the name in quotes (e.g. "my token"
)
java -jar hcs-token-example-1.0-run.jar construct TestToken TTT 8
join {topicId}
This sets up an App Net instance to join a particular Token by informing it of the Topic Id
to use and also sends a HCS transaction to inform other App Net participants of the new user's address, it should be run by anyone wanting to take part in the token.
Note: Construct
automatically adds the operator's address to the address book and sets it to be the owner of the token.
java -jar hcs-token-example-1.0-run.jar join topicId (e.g. 0.0.1234)
Note: This step could be optional, but ensures consistency of states across all App Net instances and enables verification of a recipient address in the event of a transfer.
This can be used to generate a random key pair so that you can use the public key to test transfers with.
mint {quantity}
This constructs a HCS transaction to mint the token.
When the notification is received (refresh
), the token's totalSupply
will be set to the amount specified and the balance
of the public address derived from the OPERATOR_ID
in .env
will be set to the same value.
java -jar hcs-token-example-1.0-run.jar mint 1000
This returns the name of the token from local state. If this returns empty, you may need to run refresh
.
java -jar hcs-token-example-1.0-run.jar name
This returns the symbol
of the token from local state. If this returns empty, you may need to run refresh
.
java -jar hcs-token-example-1.0-run.jar symbol
This returns the decimals
of the token from local state. If this returns empty, you may need to run refresh
.
java -jar hcs-token-example-1.0-run.jar decimals
This returns the totalSupply
of the token from local state. If this returns empty, you may need to run refresh
.
java -jar hcs-token-example-1.0-run.jar totalSupply
balanceOf {address}
This returns the balance
of the specified address from local state. If this returns a value you weren't expecting, you may need to run refresh
.
java -jar hcs-token-example-1.0-run.jar balanceOf input_your_public_key_here
transfer {address} {quantity}
This constructs a HCS transaction to transfer tokens from one address to another.
When the notification is received (refresh
), both addresses' balances are updated.
Note: The from
address is set to the public key derived from the OPERATOR_KEY
in the .env
file.
java -jar hcs-token-example-1.0-run.jar transfer 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 20
approve {spender} {amount}
This constructs a HCS transaction to approve another address as a spender up to a given amount.
When the notification is received (refresh
), the spender is added to the list of allowances.
Note: The from
address is set to the public key derived from the OPERATOR_KEY
in the .env
file.
java -jar hcs-token-example-1.0-run.jar approve 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 20
allowance {owner} {spender}
This queries local state and returns the current allowance for a given pair of addresses
java -jar hcs-token-example-1.0-run.jar allowance 302a300506032b65700321006e42135c6c7c9162a5f96f6d693677742fd0b3f160e1168cc28f2dadaa9e79cc 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232
increaseAllowance {spender} {addedValue}
This constructs a HCS transaction to increase the allowance for a given address.
When the notification is received (refresh
), the allowance for the spender
address is increased accordingly.
java -jar hcs-token-example-1.0-run.jar increaseAllowance 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 20
decreaseAllowance {spender} {addedValue}
This constructs a HCS transaction to decrease the allowance for a given address.
When the notification is received (refresh
), the allowance for the spender
address is decreased accordingly.
java -jar hcs-token-example-1.0-run.jar decreaseAllowance 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 20
transferFrom {fromAddress} {toAddress} {amount}
This constructs a HCS transaction to transfer tokens from an address using an allowance.
When the notification is received (refresh
), the balance of the fromAddress
is deducted amount
, the balance of toAddress
is increased by amount
and finally, the allowance of the operator is deducted amount
.
java -jar hcs-token-example-1.0-run.jar transferFrom 302a300506032b65700321006e42135c6c7c9162a5f96f6d693677742fd0b3f160e1168cc28f2dadaa9e79cc 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 10
burn {amount}
This constructs a HCS transaction to burn tokens from the operator's account
When the notification is received (refresh
), the balance operator's account is deducted the amount
.
java -jar hcs-token-example-1.0-run.jar burn 302a300506032b65700321006e42135c6c7c9162a5f96f6d693677742fd0b3f160e1168cc28f2dadaa9e79cc 302a300506032b65700321009308a434a9cac34e2f7ce95fc671bfbbaa4e43760880c4f1ad5a58a0b3932232 10
If you would like to pretend to be another user (or node) of the App Net, you will need to:
- Create a Hedera Account Id with sufficient funds to run HCS transactions
- Update your
.env
file with the new account details
then
java -jar hcs-token-example-1.0-run.jar join topicId (e.g. 0.0.1234)
followed by
java -jar hcs-token-example-1.0-run.jar refresh
to update your local state from notifications
From then on, any operations you perform will be performed with this user's address.
As mentioned above, local state is only updated as a result of a mirror notification. You can try this for yourself by sending a Construct
command and checking the contents of the {operator id}.json
file.
At this stage, the file should only contain the created Topic Id
.
Then, run a mint
command and check the state file again, no changes.
Finally, run a refresh
command to see the state file updated as a result of mirror notifications.
This is sample shell script to run through all the operations, to execute it, first setup two accounts on the Hedera network, then type the following in a command prompt.
export OPERATOR_SECRET_1={input your first account private key}
export OPERATOR_PUBLIC_1={input your first account public key}
export OPERATOR_ID_1={input your first account id}
export OPERATOR_SECRET_2={input your second account private key}
export OPERATOR_PUBLIC_2={input your second account public key}
export OPERATOR_ID_2={input your second account id}
if you don't set these variables up, the script will prompt you for them
then
./run.sh
The script will prompt whether you want to rebuild the project (maven). Then it will prompt whether to delete local state files (*.json). It will finally run through all the supported operations.