Ethereum And Solidity Getting Started
Ethereum networks are a set of nodes or machines mining transactions within blockchains. The purpose of each transaction is tied to a smart contract or business definition. Because the machines need power to work, this mining work has a cost or gast limit which is measured in ether (or units of ether like wei). This cost depends on the number of operations our smart contract has.
Installation
In order to invest/browse in the Ethereum networks, we can use Metamask. This is a browser extension and is intented to use only by final users (not for programming).
In order to deploy contracts and make actions over them in the Etheresum networks, we use the web3.js.
In order to design/compile our custom smart contracts, we use Solidity.
Metamask
- Install Metamask in Chrome.
- Accept terms and create an account. Save the secret phrase or mnemonic account in a safe place.
- Use the correct network: for production: the “Main Ethereum” network, it costs real money!. For testing, we’ll use the “Rinkeby Test Network” network.
The secret phrase is really like a mnemonic account and one mnemonic account can have one or many accounts that we can create later on. Each account can have different money and works in a different purpose.
How to receive Ether
Go to rinkeby-faucet and fill the address input (we can find it next to My account in Metamask plugin):
For testing, sometimes you’ll need much more money. If so, go to faucet.rinkeby.
- What happened in between?
- Click ‘submit’ on form
- Address sent to Backend server
- Backend server used web3.js library to create a ‘transaction’ object
- Backend server sent ‘transaction’ object to the Rinkeby test network
- Backend server waited for transaction to be confirmed. The transcation goes to one node in the network, the node groups this transaction with other transactions that are running at the same time into a ‘block’ and start validating (or mining!)
- Backend server sent success message back to the browser
- What is a transaction?
Field | Description |
---|---|
nonce | how many times the sender has sent a transaction |
to | address of account this money is going to |
value | amount of ether to send to the target address |
gasPrice | amount of ether the sender is willing to pay per unit gas to get this transaction processed |
startGas/gasLimit | units of gas that this transaction can consume |
v / r / s | cryptographic pieces of data that can be used to generate the senders account address. Generated from the sender’s private key |
But then, why do we need Ether’s for? Why all this complexity about the transactions? We are missing another concept: smart contracts.
Smart Contracts
Smart Contracts is a code that lives in each transaction. Actually, when deploying a new contract, this is done like another regular transaction.
Solidity Programming Language
The smart contracts are written in Solidity Programming Language.
We can test and play with the smart contracts using this online editor
Let’s write this contract:
pragma solidity^0.4.17;
contract Inbox {
address public owner;
string public message;
constructor(string initialMessage) public {
owner = msg.sender;
message = initialMessage;
}
function setMessage(string newMessage) public {
message = newMessage;
}
}
- Storage vs Memory
The storage keyword makes variables point out directly to input variable. It’s like using reference addresses in the oriented programming language world.
contract Numbers{
int[] public numbers;
function Numbers() public {
numbers.push(1);
numbers.push(2);
int[] storage other = numbers; // numbers is the input of other field.
other[0] = 3;
}
}
Output for numbers and other is the same because they are pointing to the same address location: [3, 4].
On the other hand, the memory keyworld will make a copy of the input variable in a different address location:
contract Numbers{
int[] public numbers;
function Numbers() public {
numbers.push(1);
numbers.push(2);
int[] memory other = numbers; // numbers is the input of other field.
other[0] = 3;
}
}
Now, the data in other is [3, 2] and in numbers is [1, 2].
The memory keyword is equivalent to:
contract Numbers{
int[] public numbers;
function Numbers() public {
numbers.push(1);
numbers.push(2);
changeArray(numbers);
}
function changeArray(int[] other) private {
other[0] = 3;
}
}
However, we can change this behaviour by using the storage keyword as:
contract Numbers{
int[] public numbers;
function Numbers() public {
numbers.push(1);
numbers.push(2);
changeArray(numbers);
}
function changeArray(int[] storage other) private {
other[0] = 3;
}
}
- Types
Type | Description |
---|---|
string | |
bool | |
int or int256 | positive or negative non decimal numbers). Also, we can specify the range doing int8, int16, int32, … |
uint | positive non decimal numbers |
fixed | decimal numbers |
address | example: 0x01ab1212… for accounts/clients |
fixed array | int[3] –> [1, 2, 3] |
dynamic array | int[] –> [1, 2, 3] |
- Mapping Type
This is like a key-value collection:
contract MyContract {
mapping(address => bool) public members;
function addMember() public payable {
require(msg.value > 0);
members[msg.sender] = true;
}
function isActive() public {
require(members[msg.sender]);
}
}
- Struct: Custom Type
contract MyContract {
struct MyType {
string name;
bool flag;
}
MyType public instance;
function method(string name, bool flag) public payable {
MyType memory myType = MyType({
name: name,
flag: flag
});
instance = myType;
}
}
- Message Global Context
All the messages contain the account of the sender and the transaction itself. We can access to the sender account from the smart contract by doing:
Field | Description |
---|---|
msg.data | |
msg.gas | amount of gas the current function invocation has available |
msg.sender | sender account address |
msg.value | amount of ether (in wei) that was sent along with the function invocation |
The msg instance is directly available in the smart contract constructor with doing nothing.
- Payable Function Type
Mark a public method as payable and use the require function inside as:
pragma solidity^0.4.17;
contract Inbox {
...
function doSomethingThatRequiresMoney() public payable {
require(msg.value > .01 ether, "Must have at least 0.01 ether");
// ..
}
}
- How to send money to an address
The current amount of money the contract has is in “this.balance”:
contract Lottery {
address public manager;
// ...
function sendMoneyToManager() public {
manager.transfer(this.balance);
}
}
- Modifiers
The modifiers are used to reduce the duplicity of the code we use to write our smart contracts:
function fnc1() public mymodifier {
// ...
}
function fnc2() public mymodifier {
// ...
}
modified mymodifier() {
// do the common bits and then:
_;
}
- Other functions
Function | Description |
---|---|
now | current time |
block.difficulty | a level to resolve the current block |
random | example: |
function random() private view returns (uint) {
return uint(keccak256(block.difficulty, now, an_array));
}
What about the cost of a transaction?
If we rewrite our smart contract to include a new function like:
pragma solidity^0.4.17;
contract Inbox {
...
function doMath(int a, int b) {
a + b // ADD: Costs 3 gas
a - b // SUB: Costs 3 gas
a * b // MULTIPLY: Costs 5 gas
a == b // EQ: Costs 3 gas
}
}
We can use this spreadsheet to calculate the cost of gas of our contract depending on the operations/store data. If we say in a transaction we are up to spend 10 gas unit, the above transaction could not complete.
How Do We Deploy Smart Contracts?
Earlier we said we’ll work using a test network Rinkeby, but how we deploy our smart contract into it? Let’s introduce Truffle because Remix only runs in the browser.
- Compiler: we need to setup our JS compiler to Solidity compiler
- Testing: we need to use mocha test runner and Ganache/TestRPC (local test network). Also web3 to have access to our contract in the local test network.
- Deployment: we need a deploy script. We’ll use Infura as provider.
Project Structure:
inbox ->
contracts ->
inbox.sol
test ->
inbox.test.js
package.json
compile.js
deploy.js
Steps: - Installation
npm init
npm install --save solc fs-extra
- Write compile.js
const path = require('path'); // for cross platform
const fs = require('fs-extra');
const solc = require('solc');
const buildPath = path.resolve(__dirname, 'build');
// Remove build
fs.removeSync(buildPath);
// Create build folder if it does not exist
fs.ensureDirSync(buildPath);
const inboxPath = path.resolve(__dirname, 'contracts', 'inbox.sol');
let source = fs.readFileSync(inboxPath, 'utf8');
let output = solc.compile(source, 1).contracts;
// Copy contracts inside the .sol file into the build folder
for (let contract in output) {
fs.outputJsonSync(
path.resolve(buildPath, contract.replace(':', '') + '.json'),
output[contract]
);
}
- Run compile
node compile.js
- Prepare for testing
npm install --save mocha ganache-cli web3@1.0.0-beta.26
- Write test/inbox.test.js
const assert = require('assert');
const ganache = require('ganache-cli');
const Web3 = require('web3');
const provider = ganache.provider();
const web3 = new Web3(provider);
const Inbox = require('../build/Inbox.json');
let accounts;
let inbox;
beforeEach(async () => {
// Get a list of all accounts
accounts = await web3.eth.getAccounts();
// Use one of those accounts to deploy the contract
inbox = await new web3.eth.Contract(JSON.parse(Inbox.interface))
.deploy({ data: Inbox.bytecode, arguments: [ 'Hello world!'] })
.send({ from: accounts[0], gas: '1000000' });
inbox.setProvider(provider)
});
describe('Inbox', () => {
it ('deploys a contract', () => {
// To operate with the contract, we need the address:
assert.ok(inbox.options.address);
});
it('has a default message', async () => {
const message = await inbox.methods.message().call();
assert.equal(message, 'Hello world!');
});
it('can change the message', async () => {
await inbox.methods.setMessage('This is a new message').send({ from: accounts[0], gas: '1000000' });
const message = await inbox.methods.message().call();
assert.equal(message, 'This is a new message');
});
});
Change the package json file to use mocka as test runner:
"scripts": {
"test": "mocha"
},
Run the test by doing: npm run test
- Preparing For Deployment
We want to avoid somebody modify the contract code. The best solution is to create another contract as a factory:
pragma solidity^0.4.17;
contract InboxFactory {
address[] public deployedInbox;
function createInbox(string initialMessage) public {
address newInbox = new Inbox(initialMessage); // send msg.sender if our contract wans the original sender
deployedInbox.push(newInbox);
}
function getDeployedInbox() public view returns (address[]) {
return deployedInbox;
}
}
contract Inbox {
// ...
}
- Write deployment script
We’ll use Infura and our account that we created in Metamask. Infura will find the right node in the target ethereum network and will ease the smart contract transaction.
- Signup Infura: https://infura.io/
- Create a new project (aka “Rinkeby API”) and make sure we select Rinkeby Network
- Install truffle module (this is the provider)
npm install --save truffle-hdwallet-provider
- Write the deploy.js file:
const HDWalletProvider = require('truffle-hdwallet-provider');
const Web3 = require('web3');
const Inbox = require('build/Inbox.json');
const provider = new HDWalletProvider(
'inhale require dry moment bubble deposit seed embark flock opinion fragile just', // mnemonic account
'https://rinkeby.infura.io/v3/58f26bf5af6a4a97b6d5daa81b044266'
);
const web3 = new Web3(provider);
const deploy = async () => {
const accounts = await web3.eth.getAccounts();
console.log('Attempting to deploy from account', accounts[0]); // we'll use the first account we have in the mnemonic account.
const result = await new web3.eth.Contract(JSON.parse(Inbox.interface))
.deploy({ data: '0x' + Inbox.bytecode, arguments: [ 'Hello world!'] })
.send({ gas: '1000000', from: accounts[0] });
console.log('Contract interface: ' + interface);
console.log('Contract deployed to', result.options.address); // we need the address to work with this contract!
};
deploy();
Output:
Attempting to deploy from account 0xA94044008e2C5E01A473571FBd95A357f47557bC
Contract interface: [{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]
Contract deployed to 0xF9b1a29BCc53517247cc35790b130f90C44431cF
Copy the contract address “0xdAf3aA08075E5eed4b323B87EA976B7DA27f0beC”. And also the interface. We’ll be using it later in this tutorial!
- Check your contract in: rinkeby.etherscan and use the contract address to look for. For the main network, use etherscan.
- Also, we can use remix.ethereum.org to test our deployed contract. All we need is again the address.
Frontend Development
We’ll use React for the frontend which is very easy well integrated with web3 and metamask.
- Install react in our project and create the react project
npm install -g create-react-app
create-react-app inbox-react
cd inbox-react
npm start
We’ll see our UI in localhost:3000 directly in our browser.
- Install dependencies to connect with Ethereum
npm install --save web3@1.0.0-beta.35
- Configure web3
Web3 works as a provider of Ethereum network (Rinkeby, Main or any other).
We’ll use assume the user has Metamask installed!!!!. So, note that Metamask will inject the web3 libraries in our page and there is nothing we can do in order to stop this. Therefore, we need to be sure that we’ll use the web3 version library we installed just above and then we’ll use the provider linked to the web3 Metamask version.
Let’s create a new library in src/web3.js:
import Web3 from 'web3';
const web3 = new Web3(window.web3.currentProvider); // use provider from Metamask extension;
export default web3;
And import this library in src/App.js:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import web3 from './web3';
class App extends Component {
render() {
console.log(web3.version);
web3.eth.getAccounts().then(console.log)
// ...
}
}
export default App;
- Use the inbox contract
We’ll use the same contract that we used before (run: node deploy.js and see the interface and the address again).
Create a new library src/inbox.js
import web3 from './web3';
// contract address
const address = '0xF9b1a29BCc53517247cc35790b130f90C44431cF';
// contract interface: abi
const abi = [{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}];
export default new web3.eth.Contract(abi, address);
And import the library into the src/App.js:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import web3 from './web3';
import inbox from './inbox';
class App extends Component {
state = {
owner: '',
balance: ''
};
async componentDidMount() {
const owner = await inbox.methods.owner().call();
const balance = await web3.eth.getBalance(inbox.options.address); // get the money inside of the contract
this.setState( { owner, balance });
}
render() {
console.log(web3.version);
// See accounts in Metamask
web3.eth.getAccounts().then(console.log);
return (
<div>
<h2>Inbox Contract</h2>
<p>This contract is owner by {this.state.owner}.
The balance in the contract is {web3.utils.fromWei(this.state.balance, 'ether')} ether!</p>
</div>
);
}
}
export default App;
- Let’s update the contract from UI:
// ..
class App extends Component {
state = {
owner: '',
balance: '',
message: ''
};
// ..
onSubmit = async (event) => {
event.preventDefault(); // Stop the event in the html tree.
const accounts = await web3.eth.getAccounts();
await inbox.methods.setMessage(this.state.message).send({
from: accounts[0],
gas: '1000000'
});
};
render() {
// ..
return (
<div>
<h2>Inbox Contract</h2>
<p>
This contract is owner by {this.state.owner}.
The balance in the contract is {web3.utils.fromWei(this.state.balance, 'ether')} ether!
</p>
<h2>Message: {this.state.message}</h2>
<hr />
<form onSubmit={this.onSubmit}>
<h4>Want to change the message?</h4>
<div>
<label>Type the new message</label>
<input value={this.state.message}
onChange={event => this.setState({ message: event.target.value })}>
</input>
</div>
<button>Enter</button>
</form>
</div>
);
}
}
In your contract has a payable function, you need to pass the value field in the send method by doing:
onSubmit = async (event) => {
// ..
await inbox.methods.setMessage(this.state.message).send({
from: accounts[0],
value: web3.utils.toWei(this.state.value, 'ether')
});
};
Source Code
See my Github repository for a full example.