Cryptocurrencies are topping tech news sites every day. The modern gold rush is gathering the attention of silicon valley tech companies and VCs. If you’re an engineer that is hearing executive announcements like “pivoting,” “rebranding,” and “possible ICO” you may have found this guide with good timing.
In this post, we will explore an intuitive test process for Solidity using the Truffle development kit.
This is Part Two of our three-part series on How To Launch Your Own Production-ready Cryptocurrency. The other parts are:
- Part One: Learn how to build your own cryptocurrency using the Ethereum Token Standard. A step-by-step guide for building a Solidity smart contract with Truffle and OpenZeppelin.
- Part Three: How to deploy a cryptocurrency with crowd sale capability to the Ethereum network. Web3.js development and end-to-end testing is demonstrated with the Truffle Suite.
Before we deploy our contract, we should thoroughly test it. Truffle includes Solidity test and JavaScript test documentation in their getting started guide. I’ve included sample integration tests with JavaScript.
test/integration.test.js – node.js 8 or later
/* node.js 8 or later is required */ const EthereumTx = require('ethereumjs-tx'); const Token = artifacts.require('./Token.sol'); // Default key pairs made by testrpc when using `truffle develop` CLI tool // NEVER USE THESE KEYS OUTSIDE OF THE LOCAL TEST ENVIRONMENT const privateKeys = [ 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', 'ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f', '0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1' ]; contract('Token', function(accounts) { let contract; let owner; let web3Contract; let truffle before(async () => { contract = await Token.deployed(); web3Contract = web3.eth.contract(contract.abi).at(contract.address); owner = web3Contract._eth.coinbase; let other = web3.eth.accounts[1]; }); it('should pass if contract is deployed', async function() { let name = await contract.name.call(); assert.strictEqual(name, 'Token'); }); it('should return inital token wei balance of 1*10^27', async function() { let ownerBalance = await contract.balanceOf.call(owner); ownerBalance = ownerBalance.toString(); assert.strictEqual(ownerBalance, '1e+27'); }); it('should properly [transfer] token', async function() { let recipient = web3.eth.accounts[1]; let tokenWei = 1000000; await contract.transfer(recipient, tokenWei); let ownerBalance = await contract.balanceOf.call(owner); let recipientBalance = await contract.balanceOf.call(recipient); assert.strictEqual(ownerBalance.toString(), '9.99999999999999999999e+26'); assert.strictEqual(recipientBalance.toNumber(), tokenWei); }); it('should properly return the [totalSupply] of tokens', async function() { let totalSupply = await contract.totalSupply.call(); totalSupply = totalSupply.toString(); assert.strictEqual(totalSupply, '1e+27'); }); it('should [approve] token for [transferFrom]', async function() { let approver = owner; let spender = web3.eth.accounts[2]; let originalAllowance = await contract.allowance.call(approver, spender); let tokenWei = 5000000; await contract.approve(spender, tokenWei); let resultAllowance = await contract.allowance.call(approver, spender); assert.strictEqual(originalAllowance.toNumber(), 0); assert.strictEqual(resultAllowance.toNumber(), tokenWei); }); it('should fail to [transferFrom] more than allowed', async function() { let from = owner; let to = web3.eth.accounts[2]; let spenderPrivateKey = privateKeys[2]; let tokenWei = 10000000; let allowance = await contract.allowance.call(from, to); let ownerBalance = await contract.balanceOf.call(from); let spenderBalance = await contract.balanceOf.call(to); data = web3Contract.transferFrom.getData(from, to, tokenWei); let errorMessage; try { await rawTransaction( to, spenderPrivateKey, contract.address, data, 0 ); } catch (error) { errorMessage = error.message; } assert.strictEqual( errorMessage, 'VM Exception while processing transaction: invalid opcode' ); }); it('should [transferFrom] approved tokens', async function() { let from = owner; let to = web3.eth.accounts[2]; let spenderPrivateKey = privateKeys[2]; let tokenWei = 5000000; let allowance = await contract.allowance.call(from, to); let ownerBalance = await contract.balanceOf.call(from); let spenderBalance = await contract.balanceOf.call(to); data = web3Contract.transferFrom.getData(from, to, tokenWei); let result = await rawTransaction( to, spenderPrivateKey, contract.address, data, 0 ); let allowanceAfter = await contract.allowance.call(from, to); let ownerBalanceAfter = await contract.balanceOf.call(from); let spenderBalanceAfter = await contract.balanceOf.call(to); // Correct account balances // toString() numbers that are too large for js assert.strictEqual( ownerBalance.toString(), ownerBalanceAfter.add(tokenWei).toString() ); assert.strictEqual( spenderBalance.add(tokenWei).toString(), spenderBalanceAfter.toString() ); // Proper original allowance assert.strictEqual(allowance.toNumber(), tokenWei); // All of the allowance should have been used assert.strictEqual(allowanceAfter.toNumber(), 0); // Normal transaction hash, not an error. assert.strictEqual(0, result.indexOf('0x')); }); }); /* * Call a smart contract function from any keyset in which the caller has the * private and public keys. * @param {string} senderPublicKey Public key in key pair. * @param {string} senderPrivateKey Private key in key pair. * @param {string} contractAddress Address of Solidity contract. * @param {string} data Data from the function's `getData` in web3.js. * @param {number} value Number of Ethereum wei sent in the transaction. * @return {Promise} */ function rawTransaction( senderPublicKey, senderPrivateKey, contractAddress, data, value ) { return new Promise((resolve, reject) => { let key = new Buffer(senderPrivateKey, 'hex'); let nonce = web3.toHex(web3.eth.getTransactionCount(senderPublicKey)); let gasPrice = web3.eth.gasPrice; let gasPriceHex = web3.toHex(web3.eth.estimateGas({ from: contractAddress })); let gasLimitHex = web3.toHex(5500000); let rawTx = { nonce: nonce, gasPrice: gasPriceHex, gasLimit: gasLimitHex, data: data, to: contractAddress, value: web3.toHex(value) }; let tx = new EthereumTx(rawTx); tx.sign(key); let stx = '0x' + tx.serialize().toString('hex'); web3.eth.sendRawTransaction(stx, (err, hash) => { if (err) { reject(err); } else { resolve(hash); } }); }); }
The tests here are not exhaustive, but they are a good starting point for an engineer that is learning Solidity. This test file can only be run inside of the truffle develop
environment.
In order to migrate the contract to an Ethereum client using truffle, add this file 2_deploy_contracts.js to migrations/
. This migration step is required for the tests to run.
const Token = artifacts.require('Token'); module.exports = (deployer) => { deployer.deploy(Token); };
To run the integration tests we will use the Truffle CLI:
npm i ethereumjs-tx truffle develop truffle(develop)> test
The truffle develop command boots a test blockchain on your local machine. The test network has 10 default Ethereum key pairs that each have 100 ETH every time the server is started. These keys will call the functions in your Solidity contract to test their functionality.
The test
command will execute all of the tests in the test/
folder. Explore the integration.test.js
file to learn exactly what the tests are doing to your test wallets and test blockchain.
Deploying
After you are comfortable with Solidity, network gas prices, and writing your own tests, you can put your token on a public test network. The Ropsten network is one of several public test networks used for Ethereum development. Network connections can be configured in the truffle.js
file.
const mnemonic = process.env.ethereum_mnemonic; const HDWalletProvider = require("truffle-hdwallet-provider"); require('babel-register'); require('babel-polyfill'); module.exports = { build: "npm run dev", networks: { development: { host: "127.0.0.1", port: 9545, network_id: "*" // Match any network id }, ropsten: { provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"), network_id: 3 } } };
If you do not have an Ethereum wallet of your own, I suggest you make one now. If you do not have a mnemonic and wallet, use this tool to generate one. Select ETH in the coin dropdown, and click the English button. Save the mnemonic somewhere safe and secure!
It’s a good idea to make separate wallets for test and main networks, so it’s less likely that you’ll have an ETH wasting accident. Add your Ethereum key set mnemonic to your environment variables as ethereum_mnemonic
as referenced in the above truffle config file.
## bash export ethereum_mnemonic="candy maple cake...."
If you do not have test ETH in your wallet on the Ropsten network, install the MetaMask Chrome extension to get some for free from a faucet. In order to execute a transaction in your token contract, you need to spend some ETH on fees – work that is done in the network on your behalf.
truffle migrate --network ropsten
This command will log the contract address, be sure to save it! Migrate uses the --network
flag and refers to the ropsten
key in the networks object in truffle.js
. An object for the main Ethereum network can be included like the Ropsten object. I excluded it from my truffle box code for safety reasons. See the “live” connection object in this Truffle Tutorial for the Main Ethereum network.
Once your token contract is deployed, you can review your token balance using the MetaMask Chrome extension. In the MetaMask UI, select the Ropsten Test Network from the dropdown picker on the top left, then click the tokens tab. Click the Add Token button and input your contract address that was logged from the truffle migrate
command.
MetaMask can transfer token, or you can create your own contract invoking UI with Web3.js. The development UI in part III: How To Launch Your Own Crowdsalable Cryptocurrency is a good example to get you going. Best of luck to you in your blockchain adventures!