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).
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.
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
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.
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
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)
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.
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.