ERC20 Inbound Transaction

Before getting started, make sure that you have both a Wanchain account and an Ethereum account. For these examples we will be using the keystore files directly, so make sure that you know where they are (usually in the home directory under .wanchain and .ethereum).

Note: Some methods of creating Wanchain keystore files will generate a file named with the address in mixed upper and lower case. The keythereum package used below cannot find keystore files named this way. To resolve, rename the keystore file so that the address is all lowercase.

Also, if you haven’t already, get some testnet Wanchain and ERC20 tokens sent to your accounts. You can use a faucet to get testnet coins.

Getting Started

To get things kicked off, let’s start by creating a new project directory and install some npm dependencies.

$ mkdir wanchain-erc20-crosschain
$ cd !$
$ npm init
$ npm install --save wanx web3 keythereum ethereumjs-tx wanchainjs-tx

We’ll use wanx to build cross-chain transactions, web3 to submit the transactions to the Wanchain and Ethereum networks, keythereum to access the private keys, and ethereumjs-tx and wanchainjs-tx to build and sign transactions before submitting them to the network.

To kick things off, create a new utils.js file, where we will put a couple of helper functions to send transactions on Wanchain and Ethereum.

$ vi utils.js

utils.js

const EthTx = require('ethereumjs-tx');
const WanTx = require('wanchainjs-tx');

module.exports = {
  sendRawEthTx,
  sendRawWanTx,
};

async function sendRawEthTx(web3, rawTx, fromAccount, privateKey) {

  // Get the tx count to determine next nonce
  const txCount = await web3.eth.getTransactionCount(fromAccount);

  // Add the nonce to tx
  rawTx.nonce = web3.utils.toHex(txCount);

  // Sign and serialize the tx
  const transaction = new EthTx(rawTx);
  transaction.sign(privateKey);
  const serializedTx = transaction.serialize().toString('hex');

  // Send the lock transaction on Ethereum
  const receipt = await web3.eth.sendSignedTransaction('0x' + serializedTx);

  return receipt;
}

async function sendRawWanTx(web3, rawTx, fromAccount, privateKey) {

  // Get the tx count to determine next nonce
  const txCount = await web3.eth.getTransactionCount(fromAccount);

  // Add the nonce to tx
  rawTx.nonce = web3.utils.toHex(txCount);

  // Sign and serialize the tx
  const transaction = new WanTx(rawTx);
  transaction.sign(privateKey);
  const serializedTx = transaction.serialize().toString('hex');

  // Send the lock transaction on Ethereum
  const receipt = await web3.eth.sendSignedTransaction('0x' + serializedTx);

  return receipt;
}

These two functions allow us to send raw transactions by passing in the web3 object, the raw transaction object, the account address that the transaction is to be sent from, and the sending account’s private key.

Now, let’s create a new node script named dai2wdai.js. This will be just a single file script that gets executed by calling node dai2wdai.js. At the top of the file, let’s add our dependencies.

dai2wdai.js

const WanX = require('wanx');
const Web3 = require('web3');
const keythereum = require('keythereum');
const utils = require('./utils');

Next, let’s initialize wanx and web3 objects. For the sake of simplicity, we can use Infura here for the connection to the Ethereum network.

const web3wan = new Web3(new Web3.providers.HttpProvider('http://localhost:18545'));
const web3eth = new Web3(new Web3.providers.HttpProvider('https://rinkeby.infura.io/<myToken>');

const config = {
  wanchain: { web3: web3wan },
  ethereum: { web3: web3eth },
};

const wanx = new WanX('testnet', config);

With that in place, let’s now start configuring the cross-chain transaction. We’ll do that with an objects that we’ll call opts.

const opts = {
  token: {
    eth: '0xdbf193627ee704d38495c2f5eb3afc3512eafa4c', // DAI
    wan: '0xda16e66820a3c64c34f2b35da3f5e1d1742274cb',
  },
  from: '0x4bbdfe0eb33ed498020de9286fd856f5b8331c2c',
  to: '0xa6d72746a4bb19f46c99bf19b6592828435540b0',
  value: '2101000000000000',
  storeman: {
    wan: '0x06daa9379cbe241a84a65b217a11b38fe3b4b063',
    eth: '0x41623962c5d44565de623d53eb677e0f300467d2',
  },
  redeemKey: wanx.newRedeemKey(),
}

The format of the opts is the same as the format for an Ethereum cross-chain transaction, except one difference: token has been defined with eth and wan attributes that correspond to the token contract addresses on Ethereum and Wanchain, respectively. In the above example, token is defined with the testnet contract addresses for the DAI token.

As before, since we are going from Ethereum to Wanchain, the from address should be an Ethereum address and the to address should be a Wanchain address. Make sure these two addresses are ones for which you have the keystore files. The value is the amount of DAI token that we want to send to Wanchain, priced in that tokens smallest division (that is, wei or other).

Also as before, the opts object requires a redeemKey, which can be retrieved by calling wanx.newRedeemKey(). The redeemKey includes two parts, a random string (x, which is the key needed to redeem the token on Wanchain) and the hash of the random string (xHash, which is the transaction identifier).

Before starting our transaction code, we need to set up keythereum. First let’s do this for Ethereum. (Note: this is exactly same as with the Ethereum integration)

const ethDatadir = '/home/<myUser>/.ethereum/testnet/';
const ethKeyObject = keythereum.importFromFile(opts.from, ethDatadir);
const ethPrivateKey = keythereum.recover('<myPassword>', ethKeyObject);

Make sure to put in your username for <myUser> and that the path correctly points to the directory that contains the keystore directory where your keystore file lives. Also, make sure to put in your keystore password in place of <myPassword>.

Then, let’s do the same for the Wanchain keystore.

const wanDatadir = '/home/<myUser>/.wanchain/testnet/';
const wanKeyObject = keythereum.importFromFile(opts.to, wanDatadir);
const wanPrivateKey = keythereum.recover('<myPassword>', wanKeyObject);

Even though the code we have thus far doesn’t really do anything, go ahead and run the script. If there are any issues with accessing your keystore files, you should see the errors here. Ideally you should see no output when running the script as it currently stands.

$ node dai2wdai.js

Making the Transaction

At this point the script is sufficiently set up and we are now ready to make the cross-chain transaction. To get this started, let’s initialize a new cross-chain transaction object.

// New crosschain transaction
// erc20, inbound
const cctx = wanx.newChain('erc20', true);

Before the transaction gets kicked off, let’s also log out the transaction opts. In the crude example the redeemKey is not stored anywhere, so we need to make sure to print it to stdOut so that we can capture the redeemKey, in case we need to redeem or revoke later.

console.log('Tx opts:', opts)
If the script runs perfectly you will not need to know or keep the redeemKey. But if the scripts has an error and the transaction does not complete, you will not be able to recover any lost funds without the redeemKey.

Here we initialized cctx as an inbound (inbound = true) ERC20 transaction (erc20) on the Ethereum chain. Next, let’s go ahead and add in the basic logic of the transaction. We’ll fill in the missing functions in a bit.

Promise.resolve([])
  .then(sendApprove)
  .then(sendLock)
  .then(confirmLock)
  .then(sendRedeem)
  .then(confirmRedeem)
  .catch(err => {
    console.log('Error:', err);
  });

The full logic of the transaction is handled by a chain of promises that flow through the required steps of the cross-chain transaction. First the approve is sent, and then the lock is sent and then confirmed, and then the redeem is sent and finally confirmed.

Let’s go ahead and define these 5 functions.

async function sendApprove() {

  // Get the raw approve tx
  const approveTx = cctx.buildApproveTx(opts);

  // Send the lock tx on Ethereum
  const receipt = await utils.sendRawEthTx(web3eth, approveTx, opts.from, ethPrivateKey);

  console.log('Approve sent:', receipt);
}

async function sendLock() {

  // Get the raw lock tx
  const lockTx = cctx.buildLockTx(opts);

  // Send the lock tx on Ethereum
  const receipt = await utils.sendRawEthTx(web3eth, lockTx, opts.from, ethPrivateKey);

  console.log('Lock sent:', receipt);
}

async function confirmLock() {

  // Get the current block number on Wanchain
  const blockNumber = await web3wan.eth.getBlockNumber();

  // Scan for the lock confirmation from the storeman
  const log = await cctx.listenLock(opts, blockNumber);

  console.log('Lock confirmed:', log);
}

async function sendRedeem() {

  // Get the raw redeem tx
  const redeemTx = cctx.buildRedeemTx(opts);

  // Send the redeem transaction on Wanchain
  const receipt = await utils.sendRawWanTx(web3wan, redeemTx, opts.to, wanPrivateKey);

  console.log('Redeem sent:', receipt);
}

async function confirmRedeem() {

  // Get the current block number on Ethereum
  const blockNumber = await web3eth.eth.getBlockNumber();

  // Scan for the lock confirmation from the storeman
  const log = await cctx.listenRedeem(opts, blockNumber);

  console.log('Redeem confirmed:', log);
  console.log('COMPLETE!!!');
}

The sendApprove, sendLock, and sendRedeem methods call buildApproveTx, buildLockTx, or buildRedeemTx to generate a new transaction object with the correct parameters. The transaction objects are sent to our helpers functions, which get and attach the nonce to the transaction object, signs the transaction with the private key, and then finally serializes the transaction and sends it to the network.

The listenLock and listenRedeem methods are called to poll the network for a transaction from the Storeman group that fits the transaction criteria. If you don’t want to rely on WanX to listen for those transactions, you can alternatively use buildLockScanOpts and buildRedeemScanOpts to get the parameters for the scan, and then make your own subscribe call to the network.

Note: the confirmRedeem function call is technically not needed, since the Wanchain token are sent to the recipient's account once the sendRedeem succeeds. The example adds the final step only for completeness.

With all of these functions in place, we can now run our script.

$ node dai2wdai.js

If everything goes well you should see some output and no printed errors. If your script manages to send the lock transaction but fails to send the redeem transaction, you can either try the redeem again by rerunning the script with the redeemKey manually added, and with the initial approve and lock transactions and listen lock commented out. Otherwise, once the timelock expires you can revoke the locked funds, as we will demonstrate in the next section.