In past article I’ve written about some basic stuff we can do with Ethereum client Parity – like transfering Ethers, creating multi-signature wallet and even writing our own contracts. Now I’ll continue with writing our very own Distributed Application ( Dapp).
What is Dapp?
In my understanding Distributed Application is combination of contract(s) deployed in Ethereum blockchain and browser based UI that enables easy interaction with contracts. Some of application logic that is normally hosted in server, is in Smart Contracts in the distributed system – Ethereum – so that why they are called distributed. System is shown on following picture:
So Dapp consists of two parts – browser client, written in Javasript and Smart Contract(s), written in Solidity (or other Ethereum language). Browser calls contract(s) via JSON-RPC (interface is standardized across Ethereum browsers, so Dapp browser part can theoretically work with any client). Though we can call directly JSON-RPC methods from general JavaScript, it’s much easier to use existing library web3.js, which provides convenient objects and methods.
Our Dapp – Name Registry
For our demo Dapp we use contract from previous article, but improved slightly by adding event to notify about new name registration:
pragma solidity ^0.4.0; contract Registry { struct Entry { string value; address owner; } event Register(string name, address who); mapping(string=>Entry) private map; uint public fee; address registrar; function Registry(uint initialFee) { fee = initialFee; registrar = msg.sender; } function register(string key, string value) payable { //registration has fee require(msg.value >= fee); if (map[key].owner == address(0)) { // not owned by anybody map[key] = Entry(value, msg.sender); Register(key, msg.sender); } else { // already owned by somebody // then only owner can register new value require(msg.sender == map[key].owner); map[key] = Entry(value, msg.sender); } } function transfer(string key, address to) { require(map[key].owner == msg.sender); string storage value = map[key].value; map[key] = Entry(value, to); } function query(string key) constant returns(string) { return map[key].value; } function withdraw(uint amount) { require(this.balance >= amount && registrar == msg.sender); msg.sender.transfer(amount); } }
For UI part we use Aurelia (as I have played with Aurelia framework before, so this is good opportunity to refresh knowledge), Bootstrap 3 and web3.js for communication with Ethereum client. The resulting code is here in github.
When creating this simple Dapp following two things showed as tricky:
- Web application packing – as several times before correct packing of web application was problematic. Finally web3.js worked correctly with webpack bundled application generated with Aurelia CLI and using recent version (3.5) of webpack.
- Web3.js version – initially I started with latest beta version (1.0), but it does not seem to be ready yet, documentation is brief and I was missing some functionality or it was not working properly, so I finally used stable version 0.20. This is a pity, because 1.0.0 has much modern interface, using Promises etc.
The core ES6 class for interaction with Registry
contract is client.js
:
import Web3 from 'web3'; // needs to be changed to address of actual contract const contractAddress ='0x2CdB6AE9F7B24fb636b95d9060ff0D0F20e836D6'; const contractABI = [{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"key","type":"string"},{"name":"value","type":"string"}],"name":"register","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"key","type":"string"}],"name":"query","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"key","type":"string"},{"name":"to","type":"address"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialFee","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"who","type":"address"}],"name":"Register","type":"event"}]; function toPromise(fn, ...args) { return new Promise((resolve, reject) => { if (! (typeof(fn) === 'function')) { reject("Param is not a function"); } else { try { fn(...args, (err,v) => { if (err) { reject(err) } else { resolve(v); } }) } catch(e) { reject("Function call error: "+ JSON.stringify(e)); } } }) } export class Client { constructor() { this.fee = 0; let web3; if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); } else if (window.location.pathname == "/register/") { // in parity let rpcURL = `${window.location.protocol}//${window.location.host}/rpc/`; web3 = new Web3(new Web3.providers.HttpProvider(rpcURL)); } else { // set the provider you want from Web3.providers web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); } this.web3 = web3; window.web3 = web3; // just for development this.registry = this.web3.eth.contract(contractABI).at(contractAddress); toPromise(this.registry.fee.call) .catch(e => console.log(`Fee error: ${JSON.stringify(e)}`)) .then(v =>{ this.fee = v; console.log(`Fee is ${this.fee}`); } ); this._listeners = []; let fromBlock = this.web3.eth.blockNumber-10000; this.registry.Register({},{fromBlock, toBlock:'latest'}).watch((err,data) => { if (!err) { console.log(`Got event ${JSON.stringify(data)}`) setTimeout(() => { for (let l of this._listeners) { l(data); } }, 0) } }); this._listeners = []; } addListener(fn) { this._listeners.push(fn); } get connected() { let now = new Date(); if (! this._last || (now -this._last) > 10000) { this._last = now; this._conn = this.web3.isConnected(); return this._conn; } else { return this._conn; } } query(name) { return toPromise(this.registry.query.call,name); } register(name, value) { let address = this.web3.eth.accounts[0]; // fist estimate gas in local VM let data = this.registry.register.getData(name, value); let estimatePromise = toPromise(this.web3.eth.estimateGas, { to: this.registry.address, data:data, from: address, value: this.fee}); //then send it to blockchain let sendPromise = estimatePromise .then( (gas) => { console.log(`Estimate succeded with ${gas}`); return toPromise(this.registry.register.sendTransaction, name, value, {from: address, value: this.fee, gas }) }); // and get reciept let receiptPromise = sendPromise.then(txHash => { let txSendTime = new Date() return new Promise((resolve, reject)=> { let checkReceipt = () => { this.web3.eth.getTransactionReceipt(txHash, (err,r) =>{ if (err) { reject(err) } else if (r && r.blockNumber) { resolve(r) } else if (new Date() - txSendTime > 60000) { reject(new Error(`Cannot get receipt for 60 secs, check manually for ${txHash}`)); } else { window.setTimeout(checkReceipt, 1000); } }); }; checkReceipt(); }); }); return {send: sendPromise, receipt: receiptPromise, estimate: estimatePromise}; } }
Client must know address of the contract (contractAddress
) and its interface (contractABI
). As I do prefer to work with promises rather then callbacks, there is an utility function toPromise
that converts callback to promise.
Two functions, important for user interface, are query
, which queries the registry, and register
, which registers new name. Of these two later is more interesting – the interaction with blockchain is done in 3 steps:
- Call is evaluated locally with
web3.eth.estimateGas
– it executes contract method in local EVM. More important then knowing consumed gas is the fact that call executes without problems. If we do not check this now, we can easily send transactions that will fail ( for instance trying to register already registered name), but even such transactions are sent out and included in blockchain. This early check prevents such problems. - Send out signed transaction – this step requires cooperation of Parity wallet, were transaction has to be signed.
- Get notification that transaction was included in the blockchain. Method
web3.eth.getTransactionReceipt
can return null, if there are not enough new blocks confirming our transaction ( to be reasonably sure that transaction is not in the orphaned block), so we have to try several times until receipt is available.
Method addListener
enables to listen to Register
events and update application about recently registered names.
Apart of the code above rest of the application is usual Aurelia UI stuff. Finally application looks like this:
And here is screen for querying registry:
Other tutorials
You can check for instance this Parity Dapp tutorial – which is focused particularly on Parity’s special libraries integrating with React. I went through it (with some issues, partly related again to web application packaging) and it’s really nice. Parity libraries provides cool reactive components – like TransactButton
button, which visualizes transactions steps. Such components can significantly speed up and simplify application development.
Conclusion
We only scratched surface of distributed applications development, real applications are indeed much more complex, with numerous cooperating contracts in blockchain, dynamically created contracts etc. But it’s first step and it’s not very difficult – web part is basically regular web app development with one additional library – web3.js and few gotchas – especially around sending transactions out. Smarts contracts language Solidity is also relatively easy to comprehend, but as we’ve shown in previous articles it can be deceiving and one can make fatal errors, which make distributed application vulnerable. Security review of contracts based of solid understanding of Ethereum blockchain is a must for real applications.