HRC20 With Hydrachainjs
In this chapter we will use HydrachainJS to build a NodeJS CLI tool to interact with the HRC20 token we deployed previously.
You'll need a version of
node
that supports async/await. You should be ok if your version number is greater than 8.My version is 8.6 (nothing special about this version...):
node --version
v8.6.0
You can test whether async/await is supported or not by entering into the node REPL:
$ node
Then create an async function:
> async () => { }
[AsyncFunction]
If for some reason you need to run hydrajs on a platform that does not support async/await, please create an issue.
For modern JavaScript development, you really owe it to yourself to try VSCode.
hydrachainjs
comes with static type definitions for its API, and with VSCode you get some of the most useful IDE features (e.g. type-accurate autocomplete) without the UX bloat:It is recommended to try TypeScript too. JavaScript is in fact an extremely powerful language. TypeScript is the sobered-up version, yet retaining the same dynamism and expressivity that JavaScript developers love.
Let's clone the NodeJS project to the directory
mytoken-js
:git clone https://github.com/hydra-chain/hydrachainjs-token-cli.git mytoken-js
The project dependencies are listed in
package.json
:{
...
"dependencies": {
"minimist": "^1.2.0",
"ora": "^1.3.0",
"hydrajs": "^1.4.1"
}
}
Install these dependencies:
npm install
Let's try to get the token's total supply. Run the script index.js:
node index.js supply
Error: Cannot find module './solar.json'
Oops, the script needs to load information about the contracts you have deployed.
const repo = require("./solar.json")
const myToken = new Contract(rpc, repo.contracts[
"zeppelin-solidity/contracts/token/CappedToken.sol"
])
- The
require
function loadssolar.json
as a JavaScript object.
You should link (or copy)
solar.development.json
generated in the previous chapter to the project directory as solar.json
:ln -s ~/hydrabook/examples/mytoken/solar.development.json solar.json
Now try again:
node index.js supply
supply 14000
Yay it works (hopefully).
The Solidity method we called is:
function totalSupply() public view returns(uint256)
The ABI definition (loaded from
solar.json
) is:{
"name": "totalSupply",
"type": "function",
"payable": false,
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"indexed": false
}
],
"constant": true,
"anonymous": false
}
And to call this method using JavaScript:
async function totalSupply() {
const result = await myToken.call("totalSupply")
const supply = result.outputs[0]
console.log("supply", supply.toNumber())
}
The
result
object contains other useful information aside from the returned values.Do
console.log(result)
to print it out:{ address: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
executionResult:
{ gasUsed: 21689,
excepted: 'None',
newAddress: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
output: '00000000000000000000000000000000000000000000000000000000000036b0',
codeDeposit: 0,
gasRefunded: 0,
depositSize: 0,
gasForDeposit: 0 },
transactionReceipt:
{ stateRoot: '5a0d9cd5df18165c75755f4345ca81da94f9247c1c031171fd6e2ce1a368844c',
gasUsed: 21689,
bloom: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000',
log: [] },
outputs: [ ] }
If you hover your mouse cursor over the
result
variable, you should see that its type is IContractCallDecodedResult
:export interface IContractCallDecodedResult extends IRPCCallContractResult {
outputs: any[];
}
export interface IRPCCallContractResult {
address: string;
executionResult: IExecutionResult;
transactionReceipt: {
stateRoot: string;
gasUsed: string;
bloom: string;
log: any[];
};
}
export interface IExecutionResult {
gasUsed: number;
excepted: string;
newAddress: string;
output: string;
codeDeposit: number;
gasRefunded: number;
depositSize: number;
gasForDeposit: number;
}
The
balance
subcommand checks how many tokens an account has:node index.js balance dcd32b87270aeb980333213da2549c9907e09e94
balance: 13700
The JavaScript code that implements this:
async function balanceOf(owner) {
const res = await myToken.call("balanceOf", [owner])
const balance = res.outputs[0]
console.log(`balance:`, balance.toNumber())
}
The arguments to
balanceOf
are passed in as an array.Confusingly, there are two ways to invoke a method:
send
and call
. These two names are inherited from Ethereum. A more descriptive way to name them is perhaps to call send
"commit" and call
"query".call
(or "query"): executes contract code on your own local hyrad node as a "simulation", returning results, but not changing the blockchain. This is free.send
(or "commit"): creates an actual transaction that would execute code globally on the network, changing the blockchain. This costs gas.
Next, we are going to mint some new tokens using hydrajs. And because minting token changes the blockchain, we'll use
send
.The
mint
command creates new tokens by using send
to create a new transaction. Then it waits for that transaction to confirm:node index.js mint dcd32b87270aeb980333213da2549c9907e09e94 10000
mint tx: 469d0e6a1e1a421c84cd009b983fc153aa5db7da26fa1f89837f2731fa75586c
{ amount: 0,
fee: -0.081064,
confirmations: 0,
trusted: true,
txid: '469d0e6a1e1a421c84cd009b983fc153aa5db7da26fa1f89837f2731fa75586c',
walletconflicts: [],
time: 1514442911,
timereceived: 1514442911,
'bip125-replaceable': 'no',
details:
[ { account: '',
category: 'send',
amount: 0,
vout: 0,
fee: -0.081064,
abandoned: false } ],
hex: '02000000014d195e5308764e1f64236c64b8975030dd8b8815d7cfa88ee838c029e64fa03f0200000047463043022052a137063b24e74c3953891230dae739ae3adfa2144c91805de4e46ae7c4b152021f0ccdf1b3e4dd86de7777f437447dd147955e9e112c2607bfd67ddc4e7d6e2001feffffff02000000000000000063010403400d0301284440c10f19000000000000000000000000dcd32b87270aeb980333213da2549c9907e09e94000000000000000000000000000000000000000000000000000000000000271014a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3c2606ecea8d1010000
1976a914dcd32b87270aeb980333213da2549c9907e09e9488acc3080000',
method: 'mint',
confirm: [Function: confirm] }
✔ confirm mint
We should see that the balance had increased:
node index.js balance dcd32b87270aeb980333213da2549c9907e09e94
balance: 23700
The
mint
function source code:async function mint(toAddr, amount) {
const tx = await myToken.send("mint", [toAddr, amount])
console.log("mint tx:", tx.txid)
console.log(tx)
await tx.confirm(1)
}
tx
is the transaction submitted.tx.confirm(1)
is a Promise that returns when there is one confirmation for the transaction.
Let's transfer tokens from
dcd32...9e94
to another account. The contract's transfer
method takes two arguments:_to
address is the receiver of the tokens._value
is the amount of tokens to transfer.
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
Transfer(msg.sender, _to, _value);
return true;
}
Note that the API does not require the
_from
address. It is assumed that msg.sender
is the source of the token balance to transfer from.Ah,
msg.sender
, our old nemesis.As we've learned in The Owner UTXO Address, HYDRA doesn't really have the idea of an "account". The
msg.sender
is the address of whatever UTXO that was used to pay for the transaction.To act as
dcb3...9e94
, we need to explicitly specify an UTXO that has the same address. We can do this by using the senderAddress
option.async function transfer(fromAddr, toAddr, amount) {
const tx = await myToken.send("transfer", [toAddr, amount], {
senderAddress: fromAddr,
})
console.log("transfer tx:", tx.txid)
console.log(tx)
const confirmation = tx.confirm(1)
ora.promise(confirmation, "confirm transfer")
await confirmation
}
In the above code, the third argument of
send
allows you to specify the msg.sender
. Remember to prefund this address with UTXOs.There are other options you can specify for send. The full type definition is
IContractSendRequestOptions
:export interface IContractSendRequestOptions {
amount?: number | string
gasLimit?: number
gasPrice?: number | string
senderAddress?: string
}
To test
transfer
, let's generate a new receiver address, and convert it to hex:./hydra-cli getnewaddress
qXuvswhQ9Vjza8AFj1vmUL4N531CDVoWsz
./hydra-cli gethexaddress qXuvswhQ9Vjza8AFj1vmUL4N531CDVoWsz
9d748f98e65c6875dbed7bfb6ffbeca426ff9cc6
To transfer 100 tokens from
dcb3...9e94
:node index.js transfer \
qdgznat81MfTHZUrQrLZDZteAx212X4Wjj \
9d748f98e65c6875dbed7bfb6ffbeca426ff9cc6 \
100
transfer tx: a1ba017b3974b98bf9c8edc824c3abc0ce17678a14e7cfac94b5900a290bdd07
✔ confirm transfer
Note that we MUST specify the senderAddress using base58 address format. We'll fix this in the future.
We can then verify that
9d74...9cc6
had indeed received the tokens:node index.js balance 9d748f98e65c6875dbed7bfb6ffbeca426ff9cc6
balance: 100
And that the origin account's balance decremented by 100:
node index.js balance dcd32b87270aeb980333213da2549c9907e09e94
balance: 23600
The
CappedToken
contract defines a few events. The Transfer
event is emitted whenever fund is moved from one account to another (also when minting new tokens). The Transfer
event:event Transfer(
address indexed from,
address indexed to,
uint256 value
);
Let's use qtumjs to subscribe to the stream of contract events, so we can react in a timely manner when a transfer occurs. The code:
async function streamEvents() {
console.log("Subscribed to contract events")
console.log("Ctrl-C to terminate events subscription")
myToken.onLog((entry) => {
console.log(entry)
}, { minconf: 1 })
}
Let's see it in action. Launch the
events
subscriber:node index.js events
Subscribed to contract events
Ctrl-C to terminate events subscription
The program hangs there waiting for new events. In another terminal, mint more tokens:
node index.js mint dcd32b87270aeb980333213da2549c9907e09e94 10000
mint tx: c0e3007178a1b9e05b33e770f7a0e7d084f2d06732658524be042dc0e9864cc4
Wait for a bit for confirmations. In the
events
terminal, you should see both Mint
and Transfer
events printed out:{ blockHash: 'd8135a1a0e4cddb82a6912fc7eb2bd7f717b7e85069dc2fa3b8f0f8c02acbd17',
blockNumber: 2372,
transactionHash: 'c0e3007178a1b9e05b33e770f7a0e7d084f2d06732658524be042dc0e9864cc4',
transactionIndex: 2,
from: 'dcd32b87270aeb980333213da2549c9907e09e94',
to: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
cumulativeGasUsed: 39306,
gasUsed: 39306,
contractAddress: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
topics:
[ '0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885',
'000000000000000000000000dcd32b87270aeb980333213da2549c9907e09e94' ],
data: '0000000000000000000000000000000000000000000000000000000000002710',
event:
{ type: 'Mint',
to: '0xdcd32b87270aeb980333213da2549c9907e09e94',
amount: } }
{ blockHash: 'd8135a1a0e4cddb82a6912fc7eb2bd7f717b7e85069dc2fa3b8f0f8c02acbd17',
blockNumber: 2372,
transactionHash: 'c0e3007178a1b9e05b33e770f7a0e7d084f2d06732658524be042dc0e9864cc4',
transactionIndex: 2,
from: 'dcd32b87270aeb980333213da2549c9907e09e94',
to: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
cumulativeGasUsed: 39306,
gasUsed: 39306,
contractAddress: 'a778c05f1d0f70f1133f4bbf78c1a9a7bf84aed3',
topics:
[ 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0000000000000000000000000000000000000000000000000000000000000000',
'000000000000000000000000dcd32b87270aeb980333213da2549c9907e09e94' ],
data: '0000000000000000000000000000000000000000000000000000000000002710',
event:
{ type: 'Transfer',
from: '0x0000000000000000000000000000000000000000',
to: '0xdcd32b87270aeb980333213da2549c9907e09e94',
value: } }
If you are running your own hydrad node instead of the provided docker image, you'll need to enable-logevents
for events logging to work.
In this chapter we've developed a simple NodeJS CLI tool to interact with an HRC20 contract.
- hydrajs is a Promise-based API. Use async/await to write clean asynchronous code.
call
is like "query",send
is like "commit".- Use
senderAddress
to incall
orsend
to specify themsg.owner
.
Now that you know how to use hydrajs, you are ready to build a DApp, and be on your way to fame and riches!
Last modified 2yr ago