The intended audience for this document are developers who need to build transactions that interact with HRC20 tokens.
This document consists of two parts. The first part describes how to execute HRC20 methods and watching for events by using various RPC calls on a hydrad node. The second part describes how to create HRC20 transactions using a library to achieve the same functionality.
The document assumes the reader is familiar with raw Bitcoin transactions as well as the ERC20 standard and the ABI of Ethereum.
Hydra HRC20 standard
Hydra HRC20 standard is virtually identical to Ethereum's ERC20 standard. A HRC20 token contract in Hydra typically would use the same template solidity contract as an ERC20 token. The only major exception being that Hydra HRC20 tokens typically use 8 decimal points instead of 18 in Ethereum.
Hydra inherited Qtum AAL (account abstraction layer) and examples with QRC20 tokens are also helpful to understand how HRC20 raw transactions work.
Calldata for HRC20 calls
Calldata in OP_CALL scripts must be encoded according to the rules of the Ethereum ABI, e.g. the calldata is typically prefixed by the 4-byte function identifier. Since the word size is 256 bits each value should be padded to 32 bytes.
Example of transfer calldata
If we want to transfer 1.1 tokens from an address we control to the address "HL1ah15xwmxLL75TBxfwiXpoovn6dKV72h", we first set the 4 byte prefix calldata to the transfer(address,uint256) function ("a9059cbb"). Next, we translate the receiver address to its 20-byte representation in hex: "1ae4b1d517dc7d62cec8739aa3a5a8fa10c9260d" (this can be done by using the gethexaddress RPC. After this, we should encode the number of tokens we wish to transfer. This value should be multiplied by the number of decimals of the HRC20 contract. Assuming that the contract we want to call uses 8 decimals, we would therefore encode this value as 68e7780 (1.1*10^8). Next, we left pad the receiver and amount with zeroes to 32 bytes and concatenate the arguments. We would therefore have the calldata: "a9059cbb 0000000000000000000000001ae4b1d517dc7d62cec8739aa3a5a8fa10c9260d 00000000000000000000000000000000000000000000000000000000068e7780".
Hydrad RPC
Hydrad uses the same style of RPC interface as bitcoind. Most of the RPCs available to bitcoind are available in hydrad, as well as a few Hydra specific ones. Below follows a description of a few RPCs that are useful when creating or watching HRC20 token transactions.
° waitforlogs - Used for listening for new events, e.g., in the context of HRC20 tokens it can be used to listen for new Transfer() events.
° createrawtransaction - Same functionality as the bitcoind equivalent with the addition of supporting OP_CALL transactions.
° gettransactionreceipt - Used to get a transaction receipt that provides information of any events emitted and the execution state of a transaction (e.g., gas used and whether an exception was triggered).
° callcontract- Used to perform a read-only execution of a contract. Can be used to, for example, execute balanceOf() or decimals() methods of a HRC20 contract.
° sendtocontract - Used to create and sign a contract transaction that is broadcast to the network. Can for example be used to execute the transfer() or approve() methods of a HRC20 contract.
° gethexaddress/fromhexaddress - Used convert Hydra's bitcoin-style base58 addresses to and from their 20-byte representation in hex.
Examples of HRC20 contract execution
Below follow several examples executing the various functions of a HRC20 contract. These examples are run against a hydrad node running in testnet mode. You can run these examples against a mainnet node by changing the -testnet argument to -mainnet. These examples depend on jq, python3, hydrad, and hydra-cli being installed on the target system.
Setting up a testnet hydrad node and deploying a test HRC20 contract
To run these examples, we set up a testnet hydrad node, generate some blocks and deploy a HRC20 contract below.
To check the current token balance of an address for a HRC20 contract we need to execute the balanceOf method of the HRC20 contract. This execution should be performed as a read-only operation, which means we should use the callcontract RPC. An example is displayed below of how to check the balance of a particular address. The output is in hex, padded to 32 bytes, and should be divided by the number of decimals used in the contract.
Checking the constants of a HRC20 contract (decimals, name, standard, totalSupply, symbol)
To fetch the constants for a HRC20 contract, we need to execute the various methods of the HRC20 contract. These executions should be performed as read-only operations, which means we should use the callcontract RPC. An example is displayed below of how to check the various constants of a HRC20 contract.
The example below will watch for new Transfer events emitted by a HRC20 contract indefinitely. The example does this by continuously calling the waitforlogs RPC. The waitforlogs RPC will block until new events matching the call's arguments are seen. When it has seen events emitted by the contract it returns a list of entries that we can iterate through to fetch the details of one or more events that were emitted. In the example below we watch for all transfer events emitted by a contract. Note: the hydrad node this runs against must be started with the -logevents argument.
# This would be run against a live node listening for new contract events forever
# Note that hydrad should be started with -logevents to enable event logging
MIN_CONFIRMATIONS=20
LAST_BLOCK=$(hydra-cli -testnet getblockcount)
TOPIC="ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
while true
do
RESULTS=$(hydra-cli -testnet waitforlogs $LAST_BLOCK null "{\"addresses\": [\"$HRC20_CONTRACT\"], \"topics\": [\"$TOPIC\"]}" $MIN_CONFIRMATIONS)
# Extract the events from the $RESULTS from .entries[topics]
for ENTRY in $(echo $RESULTS | jq -c ".entries[]")
do
TXID=$(echo $ENTRY | jq -r ".transactionHash")
SENDER_HEX=$(echo $ENTRY | jq -r ".topics[1]")
RECEIVER_HEX=$(echo $ENTRY | jq -r ".topics[2]")
AMOUNT_HEX=$(echo $ENTRY | jq -r ".data")
SENDER_ADDRESS=$(hydra-cli -testnet fromhexaddress ${SENDER_HEX:24:40})
RECEIVER_ADDRESS=$(hydra-cli -testnet fromhexaddress ${RECEIVER_HEX:24:40})
AMOUNT_TRANSFERRED=$(python3 -c "print(0x$AMOUNT_HEX)")
echo "TXID: $TXID"
echo "SENDER: $SENDER_ADDRESS"
echo "RECEIVER: $RECEIVER_ADDRESS"
echo "AMOUNT: $AMOUNT_TRANSFERRED"
echo ""
done
LAST_BLOCK=$(echo $RESULTS | jq -r ".nextblock")
done
Example of HRC20 transfer using an online hydrad node for signing
The easiest way to do a transfer between different addresses for a HRC20 contract is to simply use the sendtocontract RPC.
HRC20_SENDER="TauZFnmbNBNuY2ujQateDwzvL6zoxBiY3H"
HRC20_RECEIVER="TL1ah15xwmxLL75TBxfwiXpoovn6dKV72h"
HRC20_TOKEN_TRANSFER_VALUE=1.1
HRC20_DECIMALS=8
# Adjust the tokens to transfer by the decimals and store the result in hex padded to 32 bytes
HRC20_TOKEN_TRANSFER_VALUE_PADDED_HEX=$(python3 -c "print(hex(int(${HRC20_TOKEN_TRANSFER_VALUE} * 10**0x${HRC20_DECIMALS}))[2:].zfill(64))")
# Convert the base58 receiver address into its 20-byte hex-encoded equivalents
HRC20_RECEIVER_HEX=$(hydra-cli -testnet gethexaddress $HRC20_RECEIVER)
# Pad the receiver hex to 32 bytes
HRC20_RECEIVER_PADDED_HEX="000000000000000000000000${HRC20_RECEIVER_HEX}"
# The function hash of "transfer(address,uint256)" concatenated with the receiver address and number of tokens to transfer
CALLDATA="a9059cbb${HRC20_RECEIVER_PADDED_HEX}${HRC20_TOKEN_TRANSFER_VALUE_PADDED_HEX}"
# Create, sign and broadcast the transfer
hydra-cli -testnet sendtocontract $HRC20_CONTRACT $CALLDATA 0 100000 0.0000004 $HRC20_SENDER
In depth on Hydra transactions
Hydra uses Bitcoin-style transactions. The transaction format is basically identical to Bitcoin transactions with a few modifications to the Bitcoin scripting engine. Hydra uses Qtum 4 new opcodes in the scripting engine to allow for interaction with the Hydra EVM. The only relevant opcode for this document is OP_CALL (0xc2).
OP_CALL
The OP_CALL opcode allows transaction output scriptPubKeys to be used to execute smart contracts in the Hydra EVM. The format for an OP_CALL scripts is the following:
version gaslimit gasprice calldata contractaddress OP_CALL
Field name
Size in bytes
Typical value
Drscription
version
1
0x04
The version of the script, will always be 0x04 for purposes of this document.
gaslimit
variable
100000
A RPC call to determinte the gaslimit will exist in the next merge.
gasprice
variable
-
The price per unit of gas is determined by an Oracle. It has a fixed USD value (currently 0.50 USD, voted via the DGP) and is recalculated when Hydra/USD price changes, to maintain a fixed in USD transaction fee.
calldata
variable
-
The ABI encoded calldata, the same as in Ethereum, i.e., typically a 4-byte function signature followed by the ABI encoded function arguments. Identical to the same concept in Ethereum.
contractaddress
20
-
The address of the contract you wish to execute.
OP_CALL
opcode
0xc2
The OP_CALL opcode.
The script must follow the same rules for encoding as a typical bitcoin script, e.g., each value that is not an opcode will be prefixed with a push length unless it is a "smallint" (typically any bitcoin library used for creating a transaction will handle these details). Below is a transaction that contains an output that uses an OP_CALL scriptPubKey to execute a contract in Hydra.
Transaction Signing
The process of signing a raw transaction in Hydra is identical to signing a Bitcoin transaction. Typically, a raw transaction is signed using the signrawtransactionwithkey or signrawtransactionwithwallet in hydrad. Also, any Bitcoin library should be possible to be used to sign raw Hydra transactions.
Example of a raw HRC20 transfer using a Bitcoin library
The process of signing a raw transaction in Hydra is identical to signing a Bitcoin transaction. Typically, a raw transaction is signed using the signrawtransactionwithkey or signrawtransactionwithwallet in hydrad. Also, any Bitcoin library should be possible to be used to sign raw Hydra transactions.
var bitcoin = require('bitcoinjs-lib');
var testnet = bitcoin.networks.testnet;
/*
Example of transaction that creates and signs a HRC20 transfer transaction for Hydra using the bitcoin-js library.
The transaction format is nearly identical to that of Bitcoin, except for the OP_CALL output.
Note, since OP_CALL is a Hydra specific opcode, it needs to be hardcoded as 0xc2
The script below will assume that the input tx being spent is a P2PKH output.
*/
// The input tx hash and vout of the utxo we are going to spend
const TXINHASH = "e06d87ecfc8563e899118de9b9fc9deafc9aba41f2c420075bbf2c610fe160fc";
const TXINVOUT = 0;
// The WIF key of the input key. Hydra uses the same WIF format and prefx as Bitcoin
const TXINKEY = bitcoin.ECPair.fromWIF('cVNrcCHX5q318dDkpMFoLD1ack7TybxEjFB44xCsv5sW35kM7QuE', testnet);
// The value of the utxo, Hydra uses the same number of decimals as Bitcoin (8 decimals)
const TXINVALUE = 2000000000000;
// The contract address we wish to call
const HRC20_CONTRACT = "a20ee8612b8d338c55dcd03e65544339efd7cebc";
// The receiver address. Note that the SENDER is the address of the UTXO we are spending.
const HRC20_RECEIVER="qauZFnmbNBNuY2ujQateDwzvL6zoxBiY3H";
// The number of tokens we want to transfer
const HRC20_TOKEN_TRANSFER_VALUE=1n;
// The number of decimals defined by the HRC20 contract.
const HRC20_DECIMALS=8n;
// The minimum gas price is 0.00000040 Hydra, so we set the gas price to that here
const GASPRICE = 40;
// The gas schedule of Hydra is virtually identical to that of Ethereum, so a value that works for Ethereum will typically work for Hydra
const GASLIMIT = 100000;
// The OP_CALL (0xc2) opcode is Hydra specific and must therefore be hardcoded.
const OP_CALL = 0xc2;
var hrc20_receiver_hex=bitcoin.address.fromBase58Check(HRC20_RECEIVER)['hash'].toString('hex')
var hrc20_token_transfer_value_adjusted=HRC20_TOKEN_TRANSFER_VALUE * (10n**HRC20_DECIMALS);
var hrc20_token_transfer_value_adjusted_hex=("0".repeat(64) + hrc20_token_transfer_value_adjusted.toString(16)).slice(-64)
// The calldata, should be ABI encoded according the Ethereum ABI specification.
var calldata = "a9059cbb000000000000000000000000"+hrc20_receiver_hex+hrc20_token_transfer_value_adjusted_hex;
// The input value minus the gas cost/txfee
var changeValue = TXINVALUE-(GASPRICE*GASLIMIT);
// The opcall scriptPubKey on the form of "version gaslimit gasprice calldata contractaddress OP_CALL"
var contractCallScript = bitcoin.script.compile([
bitcoin.script.number.encode(4), // version
bitcoin.script.number.encode(GASLIMIT), // gas limit
bitcoin.script.number.encode(GASPRICE), // gas price
new Buffer(calldata, "hex"), // The ABI encoded calldata
new Buffer(HRC20_CONTRACT, "hex"), // the contract's address
OP_CALL // OP_CALL opcode
]);
// A normal P2PKH change output
var changeScript = bitcoin.script.compile([
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
new Buffer(bitcoin.crypto.hash160(TXINKEY.publicKey)),
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG
]);
var txb = new bitcoin.TransactionBuilder(testnet);
txb.addInput(TXINHASH, TXINVOUT);
txb.addOutput(contractCallScript, 0);
txb.addOutput(changeScript, changeValue);
txb.sign(0, TXINKEY);
var tx = txb.build();
console.log(tx.toHex());
Notes
° The first input to the transaction must be the sender (unless you use OP_SENDER style scripts, which is not covered in this document)
° Any Bitcoin library should be possible to use for signing, as long as it is possible to manually encode the OP_CALL opcode in the script.
Appendix
HRC20 Function signatures
23b872dd: transferFrom(address,address,uint256)>
dd62ed3e: allowance(address,address)
095ea7b3: approve(address,uint256)
70a08231: balanceOf(address)
313ce567: decimals()
06fdde03: name()
95d89b41: symbol()
18160ddd: totalSupply()
a9059cbb: transfer(address,uint256)
23b872dd: transferFrom(address,address,uint256)
Video demonstrating raw QRC20 transactions (Note: Hydra inherits Qtum UTXO and AAL model)
which is an already filled example for the ‘LockTrip’ HRC20 token. Just rename the “loctoken” string on line 3 with something related to your token. After that replace the values on lines 379-388 incl. with yours.
You can also use the HYDRA UI node, but make sure to pass the “-rest” and “-server” flags. Also make sure you have set the “-rpcuser”, “-rpcpassword” and “-rpcallowip=127.0.0.1” flags.
You can also set those through the hydra.conf file in the datadir of your node. Then you should associate the hydrachainjs library that you imported with your local node with this line:
const hydra = new Hydra("http://user:password@localhost:3389", repoData)
Make sure to replace the user and password values here with the ones you set on “-rpcuser” and “-rpcpassword”
Make sure to replace the string value on the following line:
const myToken = hydra.contract("loctoken")
with the one that you set in the solar.json file. Then you can add the remaining functions from lines 5-75 incl. and use them with your logic.
Go to the repository in the location that you cloned it in
cd hydrachainjs-token-cli
Install the required dependencies with npm/yarn
npm install / yarn install
The interaction with the cli is through the nodejs
How to check the total supply of an HRC20 contract
node index.js totalSupply
How to check the balance
node index.js balance <address>
Here the <address> should be in Ethereum hex format, e.g. the output from the following command in the HYDRA node RPC
./hydra-cli gethexaddress <address>
How to mint HRC20 tokens
node index.js mint <address> <amount>
Here the <address> should be in Ethereum hex format, e.g. the output from the following command in the HYDRA node RPC
./hydra-cli gethexaddress <address>
How to transfer HRC20 tokens to another address
node index.js transfer <from_address> <to_address>
Here the <from_address> should be in HYDRA base58 format (starting with ‘H’ for mainnet or with ‘T’ for testnet). The <to_address> should be in Ethereum hex format, e.g. the output from the following command in the HYDRA node RPC
./hydra-cli gethexaddress <address>
How to get HRC20 contract events
node index.js logs <from_block> <to_block>
Here the <from_block> has a default value of 0 and <to_block> has a default value ‘latest’. So a call without parameters ‘node index.js logs’ would get all events from block 0 until the latest.
How to stream print HRC20 contract events to the console