Introduction
Of course, we have all heard about 'Bitcoin' and the famous blockchain technology brought by Satoshi Nakamoto.
While Bitcoin is the first form of the real internet of money, Ethereum is the first form of 'the internet of decentralized computing trustless platform'!
The Blockchain technology allowed us to communicate over a network of trust, where there is a consensus enforced by cryptography, algorithm and power of the nodes / miners.
I won't go a lot into explaining bitcoin, ethereum or other form of cryptocurrencies. [Thanks to Google].
A well said sentence by the creator genius of Ethereum:
So well, Ethereum is about a decentralized computing platform that allows users to interact with others,
using a form of digital contract, the 'Smart Contract' that enforces specific behaviors between parties
given a decentralized contract well defined on the blockchain and immutable behavior once deployed there!
On Ethereum, we have 2 types of accounts (addresses):
- Users and Parties will have their 'user' accounts (wallet addresses) holding the ETH ether balance.
- Smart Contracts ('the immutable transparent behavior programs') address holding ETH and additional binary (EVM instruction - Contract OPCODE) structure and data (the contract state variables).
Background
Let us take an example of a 'Smart Contract' of a Lottery Entertainment Service:
- A number of people who will pay a ticket to subscribe
- Wait till the round closes
- A winner will be randomly picked from the pool!
You can see the Etherdrop live on etherdrop.app.
The old classic way is that you will never have access to centralized authority (server) behind the service to know or verify how this random user is picked, and what is happening behind the scenes, even in the presence of a human monitoring / regulations.
In this article, you will learn how to:
- Develop The Lottery Service Logic [Smart Contract] using Solidity language
- Deploy The SmartContract using NodeJs and Truffle on the Local Blockchain (Ganache) / or Mainnet Ethereum Blockchain
- Develop the Web
FrontEnd - UI - Materialized, JS, Jquery, Web3Js, TruffleJs
that supports Web3 (the next web generation) to interact with Ethereum Blockchain - Develop the Web Backend - NodeJS
- Analyse and read the Blockchain Data On Etherscan
Project Structure
Using the Code
We will go section by section [ A . B . C . D . E ]
[ A ]
- Starting our Smart Contract Development, the SmartContract is a kind of a class (in Java, C#, OOP) holding state variables (e.g., global variables) as well as functions.
- Please note that any execution of a smart contract function requires an onchain transaction from a user account address to the smart contract address pointing to the specific function (with its data arguments)
- A transaction to be executed and confirmed needs to be added to a block, which should be added to the blockchain (confirmed), e.g., 3 confirmations means 3 blocks after the transaction's block are added to the blockchain and so on (a block contains many transactions, given the block size...)
- While reading or calling a passive function (view) that does not change state variables or contract data is free and near instant.
- The miner (or node) that will pick your transaction will broadcast it for other nodes, and will pick it to be executed and added into the next block (so in the blockchain).
- The execution will take place on the miner node hardware(s) (gpu, cpu ... ) where the data of the contracts and its variables are on the blockchain which is a kind of distributed decentralized (raw file or db) including the whole network of users, contracts data
- The execution is not free as the miner is running its hardware and consuming electricity, energy, internet etc...
- The execution price in ethereum is about a 'GAS' consumption, where each OPCODE on the EVM will cost a specific amount of GAS
- The GAS price is in WEI ( 1 Ether = 10^18 Wei)
- A transaction will always include:
- GAS Limit (which is about how much GAS Maximum you are willing to spend)
- GAS Price (which is about how much you want to pay for the miner of each GAS spent)
- Please note that the GAS Limit will guarantee that no transaction (code execution) will hand or take time more than it is priced, and no bad infinite loops may live there.
- In case the miner GAS consumption reached the limit set by the transacting account, the transaction however will be mined, but will not be executed, Ethereum Amount will be reversed, but the GAS spent will not, and the contract state is rolled back! transaction status is Failed - Out Of Gas!
* Prepare your environment by installing GIT distributed version control system
* Install nodejs and node package manager
Notepad++ or any text editor is basically enough next on...
Open a commnad line terminal [make sure git, npm are installed and set as in the system path (environment variable) ]
> git clone git@bitbucket.org:braingerm/cp-etherdrop.git
> cd <project_root_folder_path>
> npm install (this will install the dependencies and truffle into node_modules from the packages.json)
* Install the local blockchain emulation node Ganache
* Run Ganache:
- It will listen by default on localhost port 7545
- It will create 10 accounts by default with menmonic phrase (with 100 ETH each)
- The mnemonic phrase can be imported in Metamask or other supporting wallet.
It is more like your account password (private keys)
Now most of the tools are ready, it is time to explain, compile and deploy the EtherDrop SmartContract,
Given the project structure, the EtherDrop smartcontract relies under ./contracts/EtherDrop.sol.
*.sol extension stands for solidity.
pragma solidity ^0.4.20;
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() public {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
......
contract EtherDrop is Pausable {
uint constant PRICE_WEI = 2e16;
......
event NewWinner(address addr, uint round, uint place, uint value, uint price);
struct history {
uint blacklist;
uint size;
uint[] rounds;
......
}
address[] private _queue;
address[] private _winners;
......
uint public _counter;
uint private _collectibles = 0;
mapping(address => history) private _history;
function currentRound() public view returns
(uint round, uint counter, uint round_users, uint price) {
return (_round, _counter, QMAX, PRICE_WEI);
}
function roundStats(uint index) public view returns
(uint round, address winner, uint position, uint block_no) {
return (index, _winners[index], _positions[index], _blocks[index]);
}
......
function() public payable whenNotPaused {
require(msg.value >= PRICE_WEI, 'Insufficient Ether');
if (_counter == QMAX) {
uint r = DMAX;
uint winpos = 0;
_blocks.push(block.number);
bytes32 _a = blockhash(block.number - 1);
for (uint i = 31; i >= 1; i--) {
if (uint8(_a[i]) >= 48 && uint8(_a[i]) <= 57) {
winpos = 10 * winpos + (uint8(_a[i]) - 48);
if (--r == 0) break;
}
}
_positions.push(winpos);
uint _reward = (QMAX * PRICE_WEI * 90) / 100;
address _winner = _queue[winpos];
_winners.push(_winner);
_winner.transfer(_reward);
......
emit NewWinner(_winner, _round, winpos, h.values[h.size - 1], _reward);
......
_counter = 0;
_round++;
}
h = _history[msg.sender];
require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
h.size++;
h.rounds.push(_round);
h.places.push(_counter);
h.values.push(msg.value);
h.prices.push(0);
......
_counter++;
}
......
}
After reading/analysing the commented functions, let's focus on the function() public payable whenNotPaused
function()
is the function keyword
this one is different from other functions, it has no name
it calls the fallback function of the contract
that means when a contract is called with no point function, it will go to the fallback if found
public
means that a function can be executed from outside the contract via a transaction
payable
is a required keyword to make a function able to accept ethereum -> to the contract balance
- Within the
fallback
function, you may notice many unassigned variable, please refer to the ethereum docs to know about global variables and transaction properties...
msg.value
contains the transaction ether sent amount (to the contract) msg.sender
contains the sender address
msg.data (bytes): complete calldata
msg.gas (uint): remaining gas - deprecated in version 0.4.21 and to be replaced by gasleft()
msg.sender (address): sender of the message (current call)
msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
msg.value (uint): number of wei sent with the message
_winner.transfer(_reward);
_winner
is a variable of type address
, so the function transfer applies to transferring the _reward
to the address
, where _reward
is a uint of size 256 bytes, in WEI unit
So the EtherDrop contract will work as:
- In a same round, only one subscription per address is allowed, else transaction is reversed:
h = _history[msg.sender];
require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
- In a round, the subscription price is 0.02 Ether (or 2e16 wei)
- When the queue is full, on the next round start the winner is picked
- Picking a user is from the upcoming block
Each block has a hash, the upcoming block hash will be used as source of randomness
A number between 1 and 1000 will be derived.
The _queue[winpos]
will pick the winner and send its reward
Compile the contract using truffle:
In the commnad line terminal [make sure you installed truffle ]
> truffle compile (this will compile the contract and show errors / warnings...)
Notice 'writing artifacts to .\build\contracts', this will write output *.json contract built specs used later on
in the web client js, to interact with the contract on the blockchain...
[ B ]
To Deploy the SmartContract
using truffle, it should be added under ./migrations./2_deploy_contracts.js:
let EtherDrop = artifacts.require("EtherDrop");
module.exports = function (deployer) {
deployer.deploy(EtherDrop);
};
Now in the root folder, notice what's in the truffle.js file:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
gas: 20500000,
gasPrice: 1100000000,
network_id: "5777"
}
}
};
Let us now deploy the contract to the blockchain (test local ganache):
in the command line go to the migrations folder :
> cd migrations
> truffle migrate
Please note the contract address (@ Deploying EtherDrop... 0x7ac26cc...
).
This is the Lottery Contract Address (on local test net of course).
It is where a participant will sent the 0.02 ETH to subscribe to a round (later on in Web UI)
You see the contract deployment transaction in ganache logs:
[ C ]
In this step, we will talk about the Web Client.
Since the name is EtherDrop, it is about a 'Drop' that will be filled when the round subscription is full.
I used Materializecss, a modern responsive front-end framework based on Material Design.
Please go there to learn more their layout, grids, columns, rows, container and styling...
We won't focus on the HTML design, let's jump to the Web3 and Js section, mainly where we interact with the smart contract and events.
Interacting with Blockchain needs a web3 provider, that most of current browsers do not support.
For that, you may install Metamask on chrome and get it pointed to localhost:
with ganache in networks settings, and import the wallet using the mnemonic phrase:
provided by ganache (in accounts sections):
Now let us check the js side:
Opening src/js/ed-client.js
... // brief
// check for web3 provider
initWeb3: () => {
if (typeof web3 !== 'undefined') {
BetClient.web3Provider = web3.currentProvider;
} else {
if (BetClient.onNoDefaultWeb3Provider) {
setTimeout(BetClient.onNoDefaultWeb3Provider, 3000);
}
// if no web3 found or metamask just use for the infura
let infura = 'https://mainnet.infura.io/plnAtKGtcoxBtY9UpS4b';
BetClient.web3Provider = new Web3.providers.HttpProvider(infura);
}
web3 = new Web3(BetClient.web3Provider);
return BetClient.initContract();
},
initContract: () => {
// load the generated in build.. EtherDrop.Json
dbg(`loading ${BetClient.jsonFile}`);
$.getJSON(BetClient.jsonFile, (data) => {
dbg('got abi');
BetClient.contracts.EtherDrop = TruffleContract(data);
BetClient.contracts.EtherDrop.setProvider(BetClient.web3Provider);
// watch out the address from the network id (1 on mainnet)
BetClient.address = data.networks[5777].address;
if (BetClient.onContractLoaded) {
BetClient.onContractLoaded();
}
BetClient.listen();
});
},
// example of read the EtherDrop Current Round details
loadRoundInfo: () => {
BetClient.contracts.EtherDrop
.deployed()
.then((instance) => {
// note that the instance is the contract EtherDrop itself
// having in (EtherDrop.sol) the function currentRound()
// that will return the round no, the counter, the price etc ...
return instance.currentRound.call();
})
.then(function (result) {
if (BetClient.onLoadRoundInfo)
// here we got the result[] of BigNumber uint256
// we may convert them to [] of Numbers
// and load them to the UI Presenter on callback
BetClient.onLoadRoundInfo($.map(result, (r) => {
return r.toNumber();
}));
})
.catch((e) => BetClient.raiseError(e, 'loadRoundInfo'));
},
... // brief
// this is the payment part in case to be handler by Metamask or Web3 Browser
participate: (amount) => {
// get a loaded account from Metamask, or trustwallet
web3.eth.getAccounts((error, accounts) => {
if (error) {
BetClient.raiseError(error, 'Participate');
} else {
// use the account to do the transaction
dbg(`accounts: ${JSON.stringify(accounts)}`);
if (accounts.length === 0) {
BetClient.raiseError('No Accounts', 'Participate');
} else {
BetClient.contracts.EtherDrop
.deployed()
.then((instance) => {
return instance.sendTransaction({
from: accounts[0],
value: amount // 0.02 ETH
}).then(function (result) {
dbg(JSON.stringify(result));
});
})
.catch((e) => BetClient.raiseError(e, 'Participate'));
}
}
});
},
// listen to the contract events: New Winner, New Participation
listen: () => {
BetClient.contracts.EtherDrop
.deployed()
.then((instance) => {
return instance; // the contract instance
})
.then((result) => {
// blockchain read filtering from the latest to pending block
const options = {fromBlock: 'latest', toBlock: 'pending'};
// register for the NewDropIn (new subscription)
result.NewDropIn({}, options).watch(function (error, result) {
if (error)
BetClient.raiseError(error, 'NewDropIn');
else if (BetClient.onNewDropIn)
BetClient.onNewDropIn(result);
});
// register for the NewWinner (and it is a new round start same time)
result.NewWinner({}, options).watch((error, result) => {
if (error)
BetClient.raiseError(error, 'NewWinner');
else if (BetClient.onNewWinner)
BetClient.onNewWinner(result);
});
})
// handle errors
.catch((e) => BetClient.raiseError(e, 'listen'));
}
};
[ D ]
Now we have talked enough, let us run the webserver locally.
We are using nodeJs - lite server (defined in the package.json dependency file) which uses bs plugin (browser sync).
I like this static pages serving server, it syncs the webpage on each file content change.
Handy for light webapp development, note the bs-config.json:
It tells the lite-server to watch and serve the files under ./src and ./build/contracts (where the EtherDrop.json is there):
{
"server": {
"baseDir": ["./src", "./build/contracts"]
}
}
Yupp, now let's run the server:
in the command line terminal type:
> npm run dev
TADA! The DApp will launch on your default browser, open it on Chrome.
Make sure metamask is well configured as mentioned before! Congrats!
[ E ]
When it is deployed in the mainnet [Network Id is 1].
In the EtherDrop.Json in the end of file, you will have to replace the network id from 5777 to 1.
Also, replace the creation transaction hash and contract address by the corresponding ones (deployed live contract, e.g., from remix IDE).
Let's have a look at the deployed SmartContract on Etherscan.io:
https://etherscan.io/address/0x81b1ff50d5bca9150700e7265f7216e65c8936e6
- Click on transactions to browser the transactions done on our EtherDrop Smart Contract.
We can see a transaction that's failing because the sender did include enough amount of GAS :( - Click On Code to view the verified contract source code (binary compilation verification):
Points of Interest
This contract is on its first release, on the next drop / round, the UI will be updated, more features will be added and the contract will be migrated and a new one will be deployed including optimization, performance enhancements and a lot of upcoming precautions. ;)
Join in and keep yourself updated! Long Live Bitcoin, Long Live Ethereum!
Thank you!