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"
  }
}

https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli/blob/23e6d0c40890075163eefacc0c66b018dc9c8bbc/package.json#L7-L9

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 loads solar.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
}

https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli/blob/5e2e162efcd8d32971e7fab6d1c843ac1c843933/solar.development.json.example#L46-L60

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())
}

https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli/blob/5e2e162efcd8d32971e7fab6d1c843ac1c843933/index.js#L15-L22

  • myToken.call("totalSupply") returns a Promise, and await 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())
}

https://github.com/qtumproject/qtumbook-mytoken-qtumjs-cli/blob/5e2e162efcd8d32971e7fab6d1c843ac1c843933/index.js#L24-L31

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 local qtumd 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 in call or send to specify the msg.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!

results matching ""

    No results matching ""