Ethereum Tutorials

SMART CONTRACTS

Web UI for "Duke of Ether"

As we have our "Hello, World!" contract out of the way, the next logical step is to add a Web interface to our "Duke of Ether" contract. After all, it is a game, an playing games from the console, or from Mist, for that mater, is not the experience our clients expect.

The current Duke contract, one we are going to use for now, is a modified version of what was described earlier in this tutorial. We are going to modify the contract by adding few events to it.

pragma solidity ^0.4.11;

contract Ownable 
{
	address m_addrOwner;

	function Ownable() 
	{
		m_addrOwner = msg.sender;
	}

	modifier onlyOwner() 
	{
		if (msg.sender != m_addrOwner) 
		{
			throw;
		}
    		_;
	}

	// ---

	function transferOwnership(address newOwner) onlyOwner 
	{
		//transferOwnershipCalled(msg.sender, newOwner);
		m_addrOwner = newOwner;
	}

}

// ------

contract DukeOfEther is ShareHolder
{
	uint m_deployedAtBlock = 0;
	string m_strNickName = "";    
	uint m_nDukeDate = 0;
	address m_addrCurrentDuke;
	uint m_nCurrentDukePaid;			// Cost current Duke paid

	function DukeOfEther() ShareHolder()
	{
		m_addrCurrentDuke = msg.sender;
		m_nCurrentDukePaid = 0;	
		m_nDukeDate = now;
		m_strNickName = "Vacant";
	}

	event updateDukeStatus(string strNickName, address indexed addrCurrentDuke, 
		uint nCurrentDukePaid, uint nMinNextBet, uint date);
	event updateDukeHistory(string strNickName, address indexed addrCurrentDuke, 
		uint nCurrentDukePaid, uint nMinNextBet, uint date);

	// ---

	function becomeDuke(string strNickName) payable
	{
		if(msg.value < getMinNextBet())
			throw;

		uint nFee = msg.value / 25;	// 4%
		addToShareHoldersProfit(nFee);

		uint nPrevDukeReceived = msg.value - nFee;
		// Add info about the prev. Duke to the history
		updateDukeHistory(m_strNickName, m_addrCurrentDuke, m_nCurrentDukePaid, 
			nPrevDukeReceived, m_nDukeDate);

		m_addrCurrentDuke.transfer(nPrevDukeReceived);
		m_addrCurrentDuke = msg.sender;
		m_nCurrentDukePaid = msg.value;

		m_nDukeDate = now;
		m_strNickName = strNickName;

		updateDukeStatus(strNickName, m_addrCurrentDuke, m_nCurrentDukePaid, 
			getMinNextBet(), now);
	}
	
	// ---
	
	function getDukeNickName() constant returns (string date) { return m_strNickName; }
	function getDukeDate() constant returns (uint date) { return m_nDukeDate; }
	function isOwner() constant returns (bool bIsOwner) { return (m_addrOwner == msg.sender); }
	function isDuke() constant returns (bool bIsDuke) { return (m_addrCurrentDuke == msg.sender); }
	function getCurrentDuke() constant returns (address addr) { return m_addrCurrentDuke; }
	function getCurrentDukePaid() constant returns (uint nPaid) { return m_nCurrentDukePaid; }
	function getMinNextBet() constant returns (uint nNextBet) 
	{
		if(m_nCurrentDukePaid == 0)
			return 1 finney;

		return  12 * m_nCurrentDukePaid / 10; 
	}
}

The HTML file will look like a dashboard, providing the important info about the contract's current state and some of its history. First of all, we introduce the updateDukeStatus() event, when fired, it updates the current price of the throne and other contract data. The implementation is in HTML file, same way as it was in "Hello, World!" contract (as the contract was changed and re-deployed, do not forget to change its address and ABI):

<!DOCTYPE html>
<html>
<head>
	<title>Duke of Ether!</title>

	<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
	<script src="https://snowcron.com/jquery-2.1.1.min.js"></script>
	
	<link rel="stylesheet" type="text/css" href="http://snowcron.com/styles.css">
</head>

<body style="background:white;margin-left:20px;margin-right:20px;" onload="init();">
	<p><h1 class="header">Duke of Ether!</h1>

		<div id="duke_info"></div>
		<div id="wrong_browser_warning">This isn't an ethereum browser</div> 
			
		<p><input type="text" id="edt_pay_to_become_duke"/>
		<button class="btn-large" type="button" value="Become Duke!" id="btn_become_duke"
			onClick="becomeDuke();">Become Duke!</button>
		
	<script>

	var nNextMinBet = 0;
	
	var Web3 = require('web3');

	var strContractAddress = "0x6BDD93c6Fe1Eb03b4C346d2ccd3299c7105AEc5d";
	var abi = [ { "constant": true, "inputs": [], "name": "getMinNextBet", 
		"outputs": [ { "name": "nNextBet", "type": "uint256", 
		"value": "1200000000000000" } ], "payable": false, "type": "function" }, 
		{ "constant": true, "inputs": [ { "name": "addr", "type": "address" } ], 
		"name": "getShareHolderBalance", "outputs": [ { "name": "nBalance", 
		"type": "uint256", "value": "0" } ], "payable": false, "type": "function" }, 
		{ "constant": false, "inputs": [ { "name": "amountInDukes", "type": "uint256" } ], 
		"name": "withdrawDukes", "outputs": [], "payable": false, "type": "function" }, 
		{ "constant": false, "inputs": [ { "name": "addr", "type": "address" } ], 
		"name": "withdrawOwner", "outputs": [], "payable": false, "type": "function" }, 
		{ "constant": false, "inputs": [], "name": "becomeDuke", "outputs": [], 
		"payable": true, "type": "function" }, { "constant": true, "inputs": [], 
		"name": "getnumberOfDukeTokensCurrentlyInPosession", "outputs": 
		[ { "name": "nTokens", "type": "uint256", "value": "0" } ], "payable": 
		false, "type": "function" }, { "constant": true, "inputs": [], "name": 
		"isOwner", "outputs": [ { "name": "bIsOwner", "type": "bool", "value": true } ], 
		"payable": false, "type": "function" }, { "constant": true, "inputs": [], 
		"name": "getOwnersMoney", "outputs": [ { "name": "nAmount", "type": 
		"uint256", "value": "0" } ], "payable": false, "type": "function" }, 
		{ "constant": true, "inputs": [], "name": "getDukePrice", "outputs": 
		[ { "name": "nPrice", "type": "uint256", "value": "1000000000000000" } ], 
		"payable": false, "type": "function" }, { "constant": false, "inputs": [], 
		"name": "shareHolderInvest", "outputs": [], "payable": true, "type": "function" }, 
		{ "constant": true, "inputs": [], "name": "getCurrentKing", "outputs": 
		[ { "name": "addr", "type": "address", "value": 
		"0x0d47fef347aedfc1a8209303025e3a4ecfb75675" } ], "payable": false, 
		"type": "function" }, { "constant": true, "inputs": [], "name": "getCurrentKingPaid", 
		"outputs": [ { "name": "nPaid", "type": "uint256", "value": "0" } ], 
		"payable": false, "type": "function" }, { "constant": true, "inputs": [], 
		"name": "getCumulativeShareHoldersProfit", "outputs": [ { "name": "nProfit", 
		"type": "uint256", "value": "0" } ], "payable": false, "type": "function" }, 
		{ "constant": true, "inputs": [ { "name": "addr", "type": "address" } ], 
		"name": "getShareHolderDukes", "outputs": [ { "name": "nDukes", "type": 
		"uint256", "value": "0" } ], "payable": false, "type": "function" }, 
		{ "constant": true, "inputs": [], "name": "isKing", "outputs": [
		{ "name": "bIsKing", "type": "bool", "value": true } ], "payable": false, 
		"type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", 
		"type": "address" } ], "name": "transferOwnership", "outputs": [], 
		"payable": false, "type": "function" }, { "inputs": [], "payable": false, 
		"type": "constructor" }, { "payable": true, "type": "fallback" }, 
		{ "anonymous": false, "inputs": [ { "indexed": false, "name": "addrCurrentKing", 
		"type": "address" }, { "indexed": false, "name": "nCurrentKingPaid", "type": "uint256" }, 
		{ "indexed": false, "name": "nMinNextBet", "type": "uint256" } ], 
		"name": "updateDukeStatus", "type": "event" } ];
	var contract = null;	
		
	//var history = [];

	function init() 
	{
		document.getElementById("wrong_browser_warning").style.display = "none";
	
		// Checks Web3 support
		if(typeof web3 !== 'undefined' && typeof Web3 !== 'undefined') 
		{
			// If there's a web3 library loaded, then make your own web3
			web3 = new Web3(web3.currentProvider);
		} 
		else if (typeof Web3 !== 'undefined') 
		{
			// If there isn't then set a provider
			web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
		} 
		else if(typeof web3 == 'undefined') 
		{
			// This isn't an ethereum browser
			document.getElementById("duke_info").style.display = "none";
			document.getElementById("wrong_browser_warning").style.display = "block";
			return;    
		}

		var duke = web3.eth.contract(abi);
		contract = duke.at(strContractAddress);	
		
		var updateDukeStatusEvent = contract.updateDukeStatus({_from: web3.eth.coinbase});
		updateDukeStatusEvent.watch(function(err, result) 
		{
			if(err) 
			{
				document.getElementById("wrong_browser_warning").style.display = "block";
				document.getElementById("wrong_browser_warning").text = err;
				return;
			}

			document.getElementById("wrong_browser_warning").style.display = "none";
			
			console.log(result.args);
			
			//var date = new Date().toISOString().slice(0, 10) + " " + new Date().toISOString().slice(11, 19);
			nNextMinBet = result.args.nMinNextBet.shift(-18).toNumber();
			$('#edt_pay_to_become_duke').val(nNextMinBet);
			
			var strStatus = "Current Duke: " + result.args.addrCurrentKing + 
				//"<br>Current date: " + date + 
				"<br>Paid: " + result.args.nCurrentKingPaid.shift(-18).toNumber() + " ether"
				+ "<br>Min. next bet: " + nNextMinBet + " ether";
			$('#duke_info').html(strStatus);
			
			//$('#events').html(strNewEvents);
		});
		
		//---
		
		nNextMinBet = contract.getMinNextBet.call().shift(-18).toNumber();
		
		var strStatus = "Current Duke: " + contract.getCurrentKing.call() +
			"<br>Paid: " + contract.getCurrentKingPaid.call().shift(-18).toNumber() + " ether"
			+ "<br>Min. next bet: " + nNextMinBet + " ether";
		
		$('#duke_info').html(strStatus);
		
		// ---
		
		$('#edt_pay_to_become_duke').val(nNextMinBet);
	}

	// ------
	
	function becomeDuke()
	{
		// My first account (at index 0) has password "password"
		// In order to send transactions to it, I have to unlock it.
		// TBD: move it to a separate function and don't keep a password in HTML 
		// (ask user to unlock it, and if connection is not encrypted (http, not https), ask to do it in geth/Mist...)
		// TBD: allow the user to select a wallet, instead of using the first one.
		// TBD: replace it all with "here is an address, send ether"
		
		document.getElementById("wrong_browser_warning").style.display = "block";
		
		var nPayToBecomeDuke = $('#edt_pay_to_become_duke').val();
		if(nPayToBecomeDuke < nNextMinBet)
		{
			document.getElementById("wrong_browser_warning").innerHTML = 
				"Amount is too small, min amount is " + nNextMinBet + " ether";
			return;
		}	
				
		var account = web3.eth.accounts[0];
		var acctBal = web3.fromWei(web3.eth.getBalance(account), "ether").toNumber();
		if(acctBal < nPayToBecomeDuke)
		{
			document.getElementById("wrong_browser_warning").innerHTML = 
				"Insufficient funds, required amount is " + nPayToBecomeDuke + " ether";
			return;
		}
				
		web3.personal.unlockAccount(account, "password", 3000);
		
		var nPay = web3.toWei(nPayToBecomeDuke, "ether");
		contract.becomeDuke.sendTransaction({from: web3.eth.coinbase, value: nPay},
			function(err, transactionHash) 
			{
				if (err)
					console.log(err);
			});
		
		var date = new Date().toISOString().slice(0, 10) + " " + new Date().toISOString().slice(11, 19);
		document.getElementById("wrong_browser_warning").innerHTML  = "Date: " + date + "; Transaction sent.";
	}
	
	</script>
	
</body>	
</html>

Let's walk through the code, making notes of the important details.

The first, and probably, the most important event is updateDukeStatus(address addrCurrentKing, uint nCurrentKingPaid, uint nMinNextBet); It takes parameters containing the current contract's state and makes it available to HTML page. In an HTML page, the event is handled whenever becomeDuke() function of a contract is being called. Note, that there is one more place in the contract where the contract's state changes, but we can not call the event from there. The reason is simple: in an HTML event handler, we use contract's address, which is available only after the contract is deployed. So, the sequence is: deploy the contract, copy (using Mist's "Show Interface" or geth or somehow else) the ABI, copy contract's address, paste it all to HTML's Javascript... And only then we can watch contract's events from HTML. And as we have no address at the moment the constructor is called, we probably shouldn't place an event there.

The way we process the event in our HTML file:

var duke = web3.eth.contract(abi);
contract = duke.at(strContractAddress);	
		
var updateDukeStatusEvent = contract.updateDukeStatus({_from: web3.eth.coinbase});
updateDukeStatusEvent.watch(function(err, result) 
{
	if(err) 
	{
		document.getElementById("wrong_browser_warning").style.display = "block";
		document.getElementById("wrong_browser_warning").text = err;
		return;
	}

	document.getElementById("wrong_browser_warning").style.display = "none";
			
	console.log(result.args);
			
	var date = new Date().toISOString().slice(0, 10) + " " + new Date().toISOString().slice(11, 19);
	var strStatus = "Current Duke: " + result.args.addrCurrentKing + "
Current date: " + date + "
Paid: " + result.args.nCurrentKingPaid.shift(-18).toNumber() + " ether" + "
Min. next bet: " + result.args.nMinNextBet.shift(-18).toNumber() + " ether"; $('#duke_info').html(strStatus); });

We use "result" variable that event provides. We add date, just to have it: I plan using the date code later on. Then we create a string from result.args.addrCurrentKing, result.args.nCurrentKingPaid and so on. Note, that uint is being passed to "result" as a BigNumber, which is part of the BigNumber library, which is part of web3.js. To get ethers from it, I had to divide the number by 10^18 (or call the conversion function of that library).

Having an event declaration in a contract and the above implementation in JavaScript is enough to test it, however, in that case we would have to use (for example) Mist to trigger an event. In other words, when we send ether to "becomeDuke()" of a contract, an event is being fired and our HTML (one we opened in a browser) updates with new data.

Obviously, we want to be able to do it all from HTML only, without using Mist.

To better understand the way we do it, let's walk through the way we run our app in our test environment. I described it before, but having a complete explanation in one place is always a good idea. First, to do testing, I use private block chain, running on the same computer. To do it, I start 2 instances of Geth (one for network, one (attached to the first one) for console and mining). I also run Mist, as some things can be done easier with it; however, if you prefer, feel free to use Geth console only, Mist is just a nice interface. Details are explained in Setting up Private Block Chain chapter.

Using Mist, I deploy the contract (miner should work for it), and call the "becomeDuke", sending it some ether. As I have a call to updateDukeStatus() inside becomeDuke(), the event is being fired.

Now, the LOCAL HTML file (duke_of_ether.htm) should be by that time opened in a browser (I use Chrome). It catches an event and updates data, accordingly.

In our "Hello, World" contract we have already used buttons, triggering contract's functions. We are going to do the same in our "Duke of Ether", except in case of "becomeDuke", we need to pass an amount the user entered to the contract, instead of hardcoding things. To handle all that, our HTML file has an additional field for an amount you are willing to pay and a button to submit a transaction (to pay).

The most important part of the JavaScript code in our HTML page is a becomeDuke() function:

function becomeDuke()
{
	// My first account (at index 0) has password "password"
	// In order to send transactions to it, I have to unlock it.
	// TBD: move it to a separate function and don't keep a password in HTML 
	// (ask user to unlock it, and if connection is not encrypted (http, not https), ask to do it in geth/Mist...)
	// TBD: allow the user to select a wallet, instead of using the first one.
	// TBD: replace it all with "here is an address, send ether"
	
	document.getElementById("wrong_browser_warning").style.display = "block";
		
	var nPayToBecomeDuke = $('#edt_pay_to_become_duke').val();
	if(nPayToBecomeDuke < nNextMinBet)
	{
		document.getElementById("wrong_browser_warning").innerHTML = 
			"Amount is too small, min amount is " + nNextMinBet + " ether";
		return;
	}	
				
	var account = web3.eth.accounts[0];
	var acctBal = web3.fromWei(web3.eth.getBalance(account), "ether").toNumber();
	if(acctBal < nPayToBecomeDuke)
	{
		document.getElementById("wrong_browser_warning").innerHTML = 
			"Insufficient funds, required amount is " + nPayToBecomeDuke + " ether";
		return;
	}
				
	web3.personal.unlockAccount(account, "password", 3000);
		
	var nPay = web3.toWei(nPayToBecomeDuke, "ether");
	contract.becomeDuke.sendTransaction({from: web3.eth.coinbase, value: nPay},
		function(err, transactionHash) 
		{
			if (err)
				console.log(err);
		});
		
	var date = new Date().toISOString().slice(0, 10) + " " + new Date().toISOString().slice(11, 19);
	document.getElementById("wrong_browser_warning").innerHTML  = "Date: " + date + "; Transaction sent.";
}

First of all, it makes sure the amount entered is higher or equal then the next min. bet, then it makes sure the wallet a visitor uses has enough money. Then it unlocks an account (a wallet), the action required to be able to pay from it. And the payment is issued, all data in wei, as this is what solidity functions expect.

Note the following (commented) text:

// My first account (at index 0) has password "password"
// In order to send transactions to it, I have to unlock it.
// TBD: move it to a separate function and don't keep a password in HTML 
// (ask user to unlock it, and if connection is not encrypted (http, not https), ask to do it in geth/Mist...)
// TBD: allow the user to select a wallet, instead of using the first one.
// TBD: replace it all with "here is an address, send ether"

As this is a private local blockchain, security is not an issue, and SO FAR I can use "password" as a passphrase to my account (same as wallet). However, imagine that I publish this HTML page in the Web: my wallet's password will be published with it! Which is probably a bad thing. Maybe not, as my wallet will remain on my personal computer, but still, not nice.

So, in a commented "TBD" (To Be Done) text I left a reminder to myself to
a) Ask the visitor for a password (to his/her wallet, after all, in a real world situation THEY are supposed to pay).
b) As the visitor may have more then one wallet in the system (you can see all wallets in Mist, or in geth by issuing a command "web3.eth.accounts"), then we need to provide a choice, as the first wallet in the list is not necessary the one a visitor wants to pay from.
c) Finally, as a visitor might not trust us enough to enter his password on our site, we may choose a different approach: we tell the visitor to pay to a contract's address from his wallet, the one he/she trusts. Of course, it will require to run Mist/geth/whatever, but from the point of view of visitor's security, it is definitely better.

This issue is handled in a final version of Duke of Ether contract's HTML page: the user is offered a choice between trusting us (for example, when the payment is small and he does not care) and not trusting us; in that case the user can pay from his wallet, not from our page.

I am not examining the HTML here, as a) changes are trivial and b) I have added charts to the page, and while being nice, they have nothing to do with smart contracts programming. Nevertheless, read the HTML code of Duke of Ether contract's HTML page for a complete solution.

Using events to access contract's data.

Events can be very useful in Ethereum programming. Not only can they be used to "connect" your contract with the Web interface, but also they provide a way of storing information in the blockchain that is way cheaper then if you would store it via class member variables. In other words, when a transaction is mined, smart contracts can emit events and write logs to the blockchain. Placing data in the log cost 8 gas per byte, while contract storage costs 20,000 gas per 32 bytes. However, logs are not accessible from contracts, which limits their use: a good example is historical data to be accessible from contract's Web UI.

In our case, we may want to increase the attractiveness of our Duke contract by allowing the user to add a nick name, so everyone knows that Terminator-2007 is the current Duke of Ether. We are going to store the history of prior Dukes (a crownline?) and display it alongside with the amounts they paid.

We will need two events in order to have this functionality. First, an event that is going to be triggered when a new Duke rises; as the mater of fact, we already have it: it is an "updateDukeStatus".

The second event is updateDukeHistory(), and it is called to add the previous Duke to the history (to the log). The idea is to, when the next Duke's money arrive, add the prev. Duke info to history, THEN to change prev Duke to next one by updating member variables of a class, and then to trigger updateDukeStatus() event with new data.

Finally, we need a "startup" sequence: when our web site is opened by the user, there are no immediate calls for "becomeDuke()" function, therefore the "updateDukeStatus" event will not be triggered. In the previous chapters we simply called the same "fill_duke_info" function on web page's "init" event and on a contract's "becomeDuke" event. We could do it because both on web page "init" and on "new Duke rises" situation we asked for the same info: the "fill_duke_info" function filled the following fields: "current Duke", "current Duke paid", and "min. next payment".

Now we want to get a list of prior transactions on the Web page "init" event, so we need a separate handler:

var prevDukeTransactions = contract.updateDukeStatus(
	{_sender: userAddress}, {fromBlock: 0, toBlock: 'latest'});
prevDukeTransactions.watch(function(err, result) 
{
	if (err) 
	{
		console.log(err)
		return;
	}

	// Fill the UI with the data received
	...
});

When the UI is rendered, prevDukeTransactions.stopWatching() should be called.

The {fromBlock: 0, toBlock: 'latest'} block tells our watcher to get data for all available blocks; this way we can get a list.

Obviously, two improvements are highly desirable. First, we don't need fromBlock to be 0, as we can supply a block number when our contract have been deployed, obviously there can be no transaction before that point.

In contract, we declare:

uint m_deployedAtBlock m_deployedAtBlock = 0;

In contract's constructor, we set:

m_deployedAtBlock = block.number;

Finally, we add a getter:

function getInitBlock() constant returns (uint nInitBlock) { return m_deployedAtBlock; }

Then in a Web page's JavaScript, we can call

var prevDukeTransactions = contract.updateDukeStatus({_sender: currentDukeAddress}, 
	{fromBlock: contract.getInitBlock(), toBlock: 'latest'});
prevDukeTransactions.watch(function(err, result) 
{
	if(err) 
	{
		document.getElementById("error_message").style.display = "block";
		document.getElementById("error_message").text = err;
		return;
	}

	document.getElementById("error_message").style.display = "none";
			
	nNextMinBet = result.args.nMinNextBet.shift(-18).toNumber();
	add_duke_info_to_history(nNextMinBet, result.args.addrCurrentDuke, 
		result.args.nCurrentDukePaid.shift(-18).toNumber());
});

The add_duke_info_to_history() adds the information to a large string (HTML-formatted the way you want) and shows it on screen.

The second improvement is using "indexed" keyword. It increases the efficiency of getting data from the blockchain's log:

event updateDukeStatus(indexed address addrCurrentDuke, uint nCurrentDukePaid, uint nMinNextBet);







(C) snowcron.com, all rights reserved

Please read the disclaimer