Writing, testing and deploying an Ethereum smart contract and its web interface Check out the accompanying presentation.
Picture a digital tattoo: a smart contract that anyone can use for free to leave a permanent message on the Ethereum blockchain.
That's what we'll be building during this workshop: a simple application that's great as an introduction to this new technology. Think Hello World but on the blockchain.
You'll learn some core concepts of blockchain and Ethereum such as smart contracts, transactions, and gas. Then, we'll build and test the contract and create a web interface to interact with it.
(an outdated version of my introductory tutorial, which covers some stuff we'll skip during the workshop, can be found here)
- Introduction
- Blockchain and the data structure
- Bitcoin and the incentive layer
- Ethereum and the world computer
- Dapps, cryptoeconomics and ICOs
- Hands-on
- What will we be building?
- Setup
- The Recorder smart contract: overview and testing
- Contract code walkthrough
- Testing code walkthrough
- Ganache: Deploying to a local testnet
- Setting up truffle and ganache
- Deploying the contract and interacting with it
- Running tests
- Infura: Deploying to the Ropsten testnet
- Testnets
- Block explorers
- MetaMask: Building a web interface
- Web-based contract interaction
- Setting up MetaMask and using a faucet
- Building a simple web app using MetaMask
This setup targets Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-1052-aws x86_64).
Install build-essential.
sudo apt-get install build-essential
Install npm.
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
Install truffle.
npm install -g truffle
Install ganache-cli.
npm install -g ganache-cli
Install truffle-hdwallet-provider.
npm install truffle-hdwallet-provider
Install the MetaMask browser extension.
The Recorder smart contract is as simple as they come. Its only functionality is to log a message into the blockchain. This is achieved through the use of Events, as explained below.
The smart contract is written in Solidity. This is a statically typed language to write Ethereum smart contracts. From the documentation:
Solidity is a contract-oriented, high-level language whose syntax is similar to that of JavaScript and it is designed to target the Ethereum Virtual Machine (EVM).
Let’s start by walking slowly through the code.
pragma solidity 0.4.24;
As per the documentation:
Source files can (and should) be annotated with a so-called version pragma to reject being compiled with future compiler versions that might introduce incompatible changes.
This line ensures that your source file won’t be compiled by a compiler with a version different from 0.4.24.
contract Recorder {}
contract
is, as the name implies, the keyword one uses to define a contract. A contract is defined with a name written in PascalCase.
event Record(
address _from,
string _message
);
Here, we’re defining an event. Events allow you write access to the Ethereum logging facilities. When called, the arguments they were invoked with get stored in the transaction’s log, which is a special data structure in the blockchain [docs]. These transaction logs can be used for storing information.
Compared to using contract storage (writing to a variable in a contract), using logs is much cheaper with regards to gas costs. However, this comes with a trade-off: contracts aren’t able to read from log data (see this great post by Consensys for more details).
Since, for our use case, we don’t need contracts to read from these logs, we’re using events instead of storing an array of strings.
function record(string message) public {
emit Record(msg.sender, message);
}
The record
method is as simple as they come. It’s a public
method, meaning it can be called externally. It takes a string
argument named message
, and all it does is emit
the Record
event with two parameters:
msg.sender
holds the address of the account which invoked the record function [docs];message
is just the argument record was invoked with.
Interaction with this particular smart contract is possible in only one way: sending a transaction at it, along with a message
parameter, which invokes the record
method. When an account A
invokes the record
method, the Record
event is called, which causes the tuple {A's address, message}
to be stored in the respective transaction’s log.
Truffle uses the Mocha testing framework, and Chai for assertions. We’ll be using this test.
It’s pretty simple as far as tests go:
Recorder.deployed()
resolves when it gets a hold of the deployedRecorder
instance- we use
instance.Record(...).get()
first, to get the list of messages written to it - we check that it has no messages, with
assert.equal(0, events.length)
- we then write one message with
instance.record("pokemon")
- and finally, we ensure the message was written, with
assert.equal(1, events.length)
Initialize truffle:
truffle init
This will create a directory structure with truffle defaults and examples.
- the config file stays in the root directory
- smart contracts go into the
contracts
folder - migrations go into the
migrations
folder - tests go into the
tests
folders
Deploy contracts:
truffle migrate --reset
Test contracts:
truffle test
Obtain contract instance:
recorder = Recorder.at(Recorder.address)
Call the record method with a message:
recorder.record("i dont even")
Read all contract events:
var filters = {}
var options = {fromBlock: 0, toBlock: "latest"}
var callback = (error,result) => (console.log(result.map((result) => (result.args._message))))
recorder.Record(filters, options).get(callback)
Initializing web3 for Infura:
var infuraHost = "https://ropsten.infura.io/API_KEY";
var web3 = new Web3(new HDWalletProvider("seed words", host));
Initializing web3 for MetaMask:
var web3 = new Web3(web3.currentProvider);
Obtaining a contract instance:
var contractABI = JSON.parse('[ { "anonymous": false, "inputs": [ { "indexed": false, "name": "_from", "type": "address" }, { "indexed": false, "name": "_message", "type": "string" } ], "name": "Record", "type": "event" }, { "constant": false, "inputs": [ { "name": "message", "type": "string" } ], "name": "record", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]');
var contractAddress = "0x271a247f671eeeb21f7c1d53e46fdb87509e6936";
var contract = new web3.eth.Contract(contractABI, contractAddress);
Writing to the contract:
contract.methods.record(message).send({
from: "0x3543f41a7e75162434dbebf6c3592abbf3432f04",
gas: 100000,
}, function(error, result){
console.log("error", error);
console.log("result", result);
});
Reading contract events:
contract.getPastEvents("Record", {fromBlock: 0, toBlock: "latest"}, console.log);
You can find links to great reads at Awesome Blockchain