ERC20 With QtumJS
In this chapter we will use qtumjs to build a NodeJS CLI tool to interact with the ERC20 token we deployed previously.
You can download the project code: qtumproject/qtumbook-mytoken-qtumjs-cli.
For now, qtumjs relies on qtumd
's RPC service, so make sure that the docker container is running:
docker run -it --rm \
--name myapp \
-v `pwd`:/dapp \
-p 9899:9899 \
-p 9888:9888 \
-p 3889:3889 \
hayeah/qtumportal
Note On NodeJS Compatibility
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
I recommend that you download the Long Term Support version (8.9.3):
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 qtumjs on a platform that does not support async/await, please create an issue.
Note on Code Editor
For modern JavaScript development, you really owe it to yourself to try VSCode. qtumjs
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:
While I am on my evangelizing soapbox, you should try TypeScript too! JavaScript is in fact an extremely powerful language, though it seems like a joke. TypeScript is the sobered-up version, yet retaining the same dynamism and expressivity that JavaScript developers love.
Now, back to QTUM :p
Setup The ERC20 CLI Project
Let's clone the NodeJS project to the directory mytoken-js
:
git clone https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli.git mytoken-js
The project dependencies are listed in package.json
:
{
...
"dependencies": {
"minimist": "^1.2.0",
"ora": "^1.3.0",
"qtumjs": "^1.4.1"
}
}
Install these dependencies:
npm install
Or
yarn install
if you prefer that. See: https://yarnpkg.com/en/docs/install
Getting The Total Supply
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.
// Load deployment information generated by solar
const repo = require("./solar.json")
// Contract needs the contract address, owner address, and ABI.
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 ~/qtumbook/examples/mytoken/solar.development.json solar.json
See an example solar.development.json file
Now try again:
node index.js supply
supply 14000
Yay it works (hopefully).
Calling A Read-Only Method
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:
// const myToken = new Contract( ... )
async function totalSupply() {
const result = await myToken.call("totalSupply")
// supply is a BigNumber instance (see: bn.js)
const supply = result.outputs[0]
console.log("supply", supply.toNumber())
}
myToken.call("totalSupply")
returns a Promise, andawait
is a syntatic sugar to that waits for the asynchronous computation, then returns the result.- Solidity numbers (int, uint, etc.) are represented in JavaScript using BigNumber.
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: [ <BN: 36b0> ] }
If you hover your mouse cursor over the result
variable, you should see that its type is IContractCallDecodedResult
:
The type definition for 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;
}
Call Method With Arguments
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])
// balance is a BigNumber instance (see: bn.js)
const balance = res.outputs[0]
console.log(`balance:`, balance.toNumber())
}
The arguments to balanceOf
are passed in as an array.
Send VS Call
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 localqtumd
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 qtumjs. And because minting token changes the blockchain, we'll use send
.
Mint Tokens With 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.
Token Transfer
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]);
// SafeMath.sub will throw if there is not enough balance.
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, QTUM 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)
// or: await tx.confirm(1)
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 {
/**
* The amount in QTUM to send. eg 0.1, default: 0
*/
amount?: number | string
/**
* gasLimit, default: 200000, max: 40000000
*/
gasLimit?: number
/**
* Qtum price per gas unit, default: 0.00000001, min:0.00000001
*/
gasPrice?: number | string
/**
* The quantum address that will be used as sender.
*/
senderAddress?: string
}
To test transfer
, let's generate a new receiver address, and convert it to hex:
qcli getnewaddress
qXuvswhQ9Vjza8AFj1vmUL4N531CDVoWsz
qcli 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. See: qtumjs issues#2
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
Observing Contract Events
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: <BN: 2710> } }
{ 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: <BN: 2710> } }
If you are running your own qtumd node instead of the provided docker image, you'll need to enable
-logevents
for events logging to work. See: qtum-docker/dapp.
Conclusion
In this chapter we've developed a simple NodeJS CLI tool to interact with an ERC20 contract.
- qtumjs 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 qtumjs, you are ready to build a DApp, and be on your way to fame and riches!