Compile/Test/Deploy/Interact with contracts by CONNEX.
sharp-cli compilesharp-cli test [npm task name]sharp-cli exec [path to script]. ├── contracts # contracts directory │ ... │ └── mvg.sol ├── output # compiled contracts meta ├── package.json ├── scripts # custom scripts ... └── tests # tests
npm i @libotony/sharp @libotony/sharp-cli --save
Configuration of sharp is located in package.json, under the namespace of sharp. For the complete guide of configuration, check sharp-cli. In this project we just need to specify the files need to to be compiled in sharp.contracts.
// package.json { "sharp": { "contracts_directory": "contracts", "contracts": [ "mvg.sol" ], "build_directory": "output", "solc": { ... // solidity compiler options } } }
Just run the following command, sharp-cli will read configurations from package.json and compile the contracts.
npm run compile
npm run test/npm test/npm t
sharp-cli exec [file] will create a running environment for user script, it is useful for developers deploying contracts or running customized scripts. In this project I made an example of deploying the contract.
npm run deploy
Command sharp-cli test [task] will start a solo node in the background and then start a npm task which is aiming to run tests. In the project, I used the well know framework mocha.
We need to setup connex first, to run the tests sharp-cli will start a solo node in the background and set the environment variable THOR_REST for us to initiate connex. See connex-loader for the detail.
import { Framework } from '@vechain/connex-framework' import { Driver, SimpleNet, SimpleWallet } from '@vechain/connex.driver-nodejs' const wallet = new SimpleWallet() // setup wallets here const genesis = {...solo genesis} const net = new SimpleNet(process.env.THOR_REST) const driver = new Driver(net, genesis, undefined, wallet) const connex = new Framework(driver)
In this part, we will need sharp to get tests written. First we need ContractMeta to manage contract meta info.
import { ContractMeta } from 'sharp' const mvgTokenContract = require('../output/MVG.json') const mvgToken = new ContractMeta(mvgTokenContract.abi, mvgTokenContract.bytecode) // Get the ABI description of method 'balanceOf' const abi0 = mvgToken.ABI('balanceOf') // Get the ABI description of event 'Transfer' const abi1 = mvgToken.ABI('Transfer', 'event') //Build the deploy clause const clause = contract .deploy() .value(100) //100wei as endowment for contract creation .asClause(arg0, arg1) //args for constructor /* For mvg */ const { txid } = await vendor .signer(addrOne) // specify the signer, it will get the total supply based on the contract logic .sign('tx') .request([mvgToken.deploy().asClause()])
After send the transaction, we need to wait for the transaction to be packed.
import { Awaiter } from 'sharp' const receipt = await Awaiter.receipt(thor.transaction(txid), thor.ticker())
We should assert the emitted contract address, revert status, event log account, event log emitted in the constructor.
assert.isFalse(receipt.reverted, 'Should not be reverted') assert.equal(receipt.outputs[0].events.length, 2, 'Clause#0 should emit two events') // output#0 should have contractAddress emitted assert.isTrue(!!receipt.outputs[0].contractAddress) Assertion // abi of the event .event(mvgToken.ABI('Transfer', 'event')) // the event log should be emitted by the contract .by(address) // mint from address 0, total supply is 1 billion .logs(zeroAddress, addrOne, toWei(1e11)) // event located at output#0.event#1 // first event of deploy clause is emitted from prototype .equal(receipt.outputs[0].events[1])
First read the total supply of the token:
const ret = await thor.account(address) .method(mvgToken.ABI('totalSupply')) .call() Assertion .method(mvgToken.ABI('totalSupply')) // calling method should return total supply of 1 billion .outputs(toWei(1e11)) .equal(ret)
Calling a method which will change the state will not change the statue but you will get the output of this action. And you will get the output immediately without waiting for the nodes pack it in to the block.
const ret = await thor.account(address) .method(mvgToken.ABI('transfer')) .caller(addrOne) .call(addrTwo, toWei(100)) assert.isFalse(ret.reverted, 'Should not be reverted') assert.equal(ret.events.length, 1, 'Output should emit one event') Assertion .event(mvgToken.ABI('Transfer', 'event')) .by(address) .logs(addrOne, addrTwo, toWei(100)) .equal(receipt.events[0])
But I want to read the state after this call, then we'll send the tx and read the state.
/* Send the transaction */ const { txid } = await vendor.sign('tx') .signer(addrOne) .request([ thor.account(address) .method(mvgToken.ABI('transfer')) .asClause(addrTwo, toWei(100)) ]) const receipt = await Awaiter.receipt(thor.transaction(txid), thor.ticker()) assert.isFalse(receipt.reverted, 'Should not be reverted') assert.equal(receipt.outputs[0].events.length, 1, 'Clause#0 should emit one event') Assertion .event(mvgToken.ABI('Transfer', 'event')) .by(address) .logs(addrOne, addrTwo, toWei(100)) .equal(receipt.outputs[0].events[0]) /* Check addrOne balance */ const ret = await thor.account(address) .method(mvgToken.ABI('balanceOf')) .call(addrOne) Assertion .method(mvgToken.ABI('balanceOf')) .outputs(toWei(1e11 - 100)) .equal(ret) /* Check addrTwo balance */ const ret = await thor.account(address) .method(mvgToken.ABI('balanceOf')) .call(addrTwo) Assertion .method(mvgToken.ABI('balanceOf')) .outputs(toWei(100)) .equal(ret)
In the early age of writing contracts, we even don't which part revert of a method failed. Luckily we got revert after that.
const ret = await thor.account(address) .method(mvgToken.ABI('transfer')) .caller(addrOne) .call(zeroAddress, toWei(100)) Assertion .revert() .with('VIP180: transfer to the zero address') .equal(ret)
const { txid } = await vendor.sign('tx') .signer(addrOne) .request([{ to: addrTwo, value: toWei(100) }]) const receipt = await Awaiter.receipt(thor.transaction(txid), thor.ticker()) assert.isFalse(receipt.reverted, 'Should not be reverted') assert.equal(receipt.outputs[0].transfers.length, 1, 'Clause#0 should emit one transfer log') Assertion .transfer() .logs(addrOne, addrTwo, toWei(100)) .equal(receipt.outputs[0].transfers[0])
Setting up the npm task is just the same as running tests of JS/TS project. The only difference is you need set the test to sharp-cli test [npm task].
// package.json { "scripts": { "test": "sharp-cli test sharp", "sharp": "mocha './tests/mvg.test.ts'", } }
In this project we write test codes in typescript, so we need require the register for TS.
{ "sharp": "mocha --require ts-node/register './tests/mvg.test.ts'", }
You may find out mocha will not exist after all tests are done, simply specify --exit to force mocha to quit after tests complete.
{ "sharp": "mocha --require ts-node/register --exit './tests/mvg.test.ts'", }
Then, npm test will work as we expected.
The full detailed contract tests are in tests folder.
sharp-cli exec [file] will expose connex and wallet in the global context of node which will make developers feel like executing the script in the sync.
For the script, sharp expects it export a function as the default export:
// CommonJS module.exports = function async(){ } // ECMAScript module const main = async ()=>{ } export default main
Here we write a script deploying the contract as an example:
// Import the script typings of extended global context, only necessary in typescript import 'sharp-cli/script' import { ContractMeta, Awaiter } from 'sharp' const mvgTokenContract = require('../output/MVG.json') const mvgToken = new ContractMeta(mvgTokenContract.abi, mvgTokenContract.bytecode) const thor = global.connex.thor const vendor = global.connex.vendor const wallet = global.wallet // Set up wallets, the private key is sensitive information, you may need to get from environment // wallet.import(process.env['ACC_PRIV']) wallet.import('...') const main = async () => { const { txid } = await vendor .sign('tx') .request([mvgToken.deploy().asClause()]) console.log(`tx sent: ${txid}, waiting receipt......`) const receipt = await Awaiter.receipt(thor.transaction(txid), thor.ticker()) if (receipt.reverted) { console.log('Failed to deploy contract') } else { console.log('Contract deployed at ' + receipt.outputs[0].contractAddress) } }
Then setup the script in NPM script:
// package.json { "scripts": { "deploy": "sharp-cli exec scripts/deploy-mvg.ts" } }
Add the register of TS:
// package.json { "deploy": "sharp-cli exec scripts/deploy-mvg.ts --require ts-node/register" }
see .travis.yml