Nemanja Grubor
20 Sep 2021
•
12 min read
In this article, we will be going to talk about the basics of Ethereum decentralized applications (DApps). The article is for people who would like to learn the basics of Ethereum DApps - theory, and implementation-wise.
Note that the project implementation is done on Windows.
Ethereum blockchain provides computation and storage capabilities using smart contracts. From there, Ethereum DApps can deploy smart contracts to use the capabilities provided by Ethereum to implement business logic.
Three architecture types are adopted by Ethereum DApps:
For DApps of the direct architecture, the client directly interacts with smart contracts deployed on the Ethereum. DApps of the indirect architecture have back-end services running on a centralized server, and the client interacts with smart contracts through the server. DApps of the mixed architecture combines the preceding two architectures where the client interacts with smart contracts both directly and indirectly through a back-end server.
DApps are divided into 17 categories:
Exchanges
Games
Finance
Gambling
Development
Storage
High-risk
Wallet
Governance
Property
Identity
Media
Social
Security
Energy
Insurance
Health
The cost of smart contracts in DApps includes two parts:
Deployments and executions are done as transactions, which cost gas. Gas is paid with Ethers, and the amount of gas used is a measurement of the complexity of contract execution. An account sends some gas in contract execution and then gets the gas left when the contract execution is confirmed. If the transaction has used all the gas sent from the initiator, the account receives an error information ”out of gas” and loses all gas it sends.
To lower the costs of deployments and executions is important. In the Ethereum blockchain, the total gas of a block is limited. A complex smart contract may cost too much gas so that it cannot be deployed, i.e., the block will not contain the transaction. In addition, the higher the contract execution costs are, the lower the throughput of contract executions, and the longer users wait for confirmations of executions.
Now, as we have learned the basic DApp theory, we will be going to implement an example of it.
Implement a DApp for children support organization donations, based on Ethereum blockchain.
Functionalities:
For this project, we will be going to use the following technology stack:
Ganache (Truffle Suite) is a personal blockchain for Ethereum application development. Ganache comes in two flavors: UI and CLI. It is more user-friendly to use Ganache UI. All versions of Ganache are available for Windows, Mac, and Linux.
Note: Ganache can't be run on 32-bit architecture (at least for Windows).
MetaMask is a browser extension (tested on Firefox and Chrome browsers). It has everything you need to manage your digital assets. Also, it provides a secure way to connect to blockchain-based applications.
Node.js is an asynchronous event-driven JavaScript runtime.
After installing Node.js, we will be going to install the following tools and dependencies:
Use the following code in the command line to install and check the Solidity compiler:
npm install -g solc
solcjs --version
We will need a truffle-config.js
file, which is a configuration file.
truffle-config.js
file is given as follows:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*" // Match any network id
},
},
contracts_directory: './src/contracts/',
contracts_build_directory: './src/abis/',
compilers: {
solc: {
optimizer: {
enabled: true,
runs: 200
},
evmVersion: "petersburg"
}
}
}
Then, we will be going to install Truffle, React.js, and Web3.js via the following package.json
file:
{
"name": "dapp",
"version": "1.0.0",
"description": "children-support-dapp",
"main": "truffle-config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "react-scripts start"
},
"author": "<your email address>",
"license": "ISC",
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"dependencies": {
"@truffle/hdwallet-provider": "^1.5.0",
"babel-polyfill": "6.26.0",
"babel-preset-env": "1.7.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-stage-2": "6.24.1",
"babel-preset-stage-3": "6.24.1",
"babel-register": "6.26.0",
"bootstrap": "4.3.1",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"chai-bignumber": "3.0.0",
"ganache-cli": "^6.12.2",
"identicon.js": "^2.3.3",
"parity": "^0.2.7",
"react": "16.8.4",
"react-bootstrap": "1.0.0-beta.5",
"react-dom": "16.8.4",
"react-scripts": "2.1.3",
"truffle": "5.1.39",
"web3": "1.2.11"
}
}
Now that package.json
file is created, it is possible to install dependencies from it to the projects folder. Installation is done via command line, using the following code:
npm install
This will create a new folder, called node_modules, that stores all installed dependencies.
In this article, it is possible to see the complete file structure of this project.
project
│ migrations
└───1_initial_migration.js
└───2_deploy_contracts.js
│ node_modules
│ public
└───favicon.ico
└───index.html
└───manifest.json
│ src
└───abis
└───automatically generated .json files after compiling
└───components
└───App.css
└───App.js
└───Main.js
└───Navbar.js
└───contracts
└───DaiToken.sol
└───Migrations.sol
└───TokenFarm.sol
└───index.js
└───serviceWorker.js
└───dai.png
└───eth-logo.png
└───farmer.png
└───logo.png
└───token-logo.png
│ package.json
│ truffle-config.js
Simply explained, smart contracts are the back-end of a blockchain application. Here, we will be going to use Solidity language for their implementation.
As the name says, Migrations.sol
smart contract is used for keeping track of which migrations were done on the current network.
Here is the code:
pragma solidity >=0.4.21 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}
DaiToken.sol
smart contract is used for the creation of the DaiToken
class, which deals with DAI cryptocurrency, with basic operations (like transfer()
).
Here is the code:
pragma solidity ^0.5.0;
contract DaiToken {
string public name = "Mock DAI Token";
string public symbol = "mDAI";
uint256 public totalSupply = 1000000000000000000000000; // 1 million tokens
uint8 public decimals = 18;
event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor() public {
balanceOf[msg.sender] = totalSupply;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= balanceOf[_from]);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}
TokenFarm.sol
smart contract requires the previously created DaiToken.sol
smart contract. It is used for storing assets, e.g. in a bank, to be able to have some assets for donating.
Here is the code:
pragma solidity ^0.5.0;
import "./DaiToken.sol";
contract TokenFarm {
string public name = "Dapp Token Farm";
address public owner;
DaiToken public daiToken;
address[] public donors;
mapping(address => uint) public donatingBalance;
mapping(address => bool) public hasDonated;
mapping(address => bool) public isDonating;
constructor(DaiToken _daiToken) public {
daiToken = _daiToken;
owner = msg.sender;
}
function donateTokens(uint _amount) public {
// Require amount greater than 0
require(_amount > 0, "amount cannot be 0");
// Trasnfer Mock Dai tokens to this contract for donating
daiToken.transferFrom(msg.sender, address(this), _amount);
// Update donating balance
donatingBalance[msg.sender] = donatingBalance[msg.sender] + _amount;
// Add user to donors array *only* if they haven't donated already
if(!hasDonated[msg.sender]) {
donors.push(msg.sender);
}
// Update donating status
isDonating[msg.sender] = true;
hasDonated[msg.sender] = true;
}
}
After creating smart contracts, you should compile them to see if there are errors. This is done via the command line. Open command prompt, and position to the abis
folder.
Run the following command:
truffle compile
If there are no errors, run the following command to migrate smart contracts:
truffle migrate
As seen in the file structure, the Migrations
folder contains two .js
files:
1_initial_migration.js
2_deploy_contracts.js
These two files serve for migrating/deployment of smart contracts.
Code for 1_initial_migration.js
:
const Migrations = artifacts.require("Migrations");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
Code for 2_deploy_contracts.js
:
const DaiToken = artifacts.require('DaiToken')
const TokenFarm = artifacts.require('TokenFarm')
module.exports = async function(deployer, network, accounts) {
// Deploy Mock DAI Token
await deployer.deploy(DaiToken)
const daiToken = await DaiToken.deployed()
// Deploy TokenFarm
await deployer.deploy(TokenFarm, /*dappToken.address,*/ daiToken.address)
const tokenFarm = await TokenFarm.deployed()
// Transfer all tokens to TokenFarm (1 million)
//await dappToken.transfer(tokenFarm.address, '1000000000000000000000000')
// Transfer 100 Mock DAI tokens to donator
await daiToken.transfer(accounts[1], '100000000000000000000')
}
The manifest.json
is a simple .json
file in a website that tells the browser about a website.
Here is the code:
{
"short_name": "Starter Kit",
"name": "Dapp Token Farm",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": # 000000",
"background_color": # ffffff"
}
The index.html
calls the previously created manifest.json
file.
Here is the code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content=# 000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Children Support Organization</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
The favicon.ico
file is an image.
A service worker is a script that a browser runs in the background (operations like push notifications and background sync).
Here is the code:
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker.'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
Note: .png
files in the src
folder are images, so we won't explain them.
The components
folder contains .js
files, and an empty .css
file, that only needs to exist.
The navigation bar contains the currently selected account, that is integrated from Ganache and MetaMask. This integration of Ganache and MetaMask will be shown later.
Here is the code for the navigation bar displaying:
import React, { Component } from 'react'
import farmer from '../farmer.png'
class Navbar extends Component {
render() {
return (
<nav className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<a
href="<some URL>"
className="navbar-brand col-sm-3 col-md-2 mr-0"
target="_blank"
rel="noopener noreferrer"
>
<img src={farmer} width="30" height="30" className="d-inline-block align-top" alt="" />
Children Support Organization
</a>
<ul className="navbar-nav px-3">
<li className="nav-item text-nowrap d-none d-sm-none d-sm-block">
<small className="text-secondary">
<small id="account">{this.props.account}</small>
</small>
</li>
</ul>
</nav>
);
}
}
The Main.js
file contains the body of the application (front-end).
It displays:
Here is the code:
import React, { Component } from 'react'
import dai from '../dai.png'
class Main extends Component {
render() {
return (
<div id="content" className="mt-3">
<table className="table table-borderless text-muted text-center">
<thead>
<tr>
<th scope="col">Donation Balance</th>
</tr>
</thead>
<tbody>
<tr>
<td>{window.web3.utils.fromWei(this.props.donatingBalance, 'Ether')} DAI</td>
</tr>
</tbody>
</table>
<div className="card mb-4" >
<div className="card-body">
<form className="mb-3" onSubmit={(event) => {
event.preventDefault()
let amount
amount = this.input.value.toString()
amount = window.web3.utils.toWei(amount, 'Ether')
this.props.donateTokens(amount)
}}>
<div>
<label className="float-left"><b>Donate tokens</b></label>
<span className="float-right text-muted">
Balance: {window.web3.utils.fromWei(this.props.daiTokenBalance, 'Ether')}
</span>
</div>
<div className="input-group mb-4">
<input
type="text"
ref={(input) => { this.input = input }}
className="form-control form-control-lg"
placeholder="0"
required />
<div className="input-group-append">
<div className="input-group-text">
<img src={dai} height='32' alt=""/>
DAI
</div>
</div>
</div>
<button type="submit" className="btn btn-primary btn-block btn-lg">Donate</button>
</form>
</div>
</div>
</div>
);
}
}
export default Main;
The App.js
file loads created smart contracts and imports files for the navigation bar (Navbar.js
) and Main.js
.
Here is the code:
import React, { Component } from 'react'
import Web3 from 'web3'
import DaiToken from '../abis/DaiToken.json'
import TokenFarm from '../abis/TokenFarm.json'
import Navbar from './Navbar'
import Main from './Main'
import './App.css'
class App extends Component {
async componentWillMount() {
await this.loadWeb3()
await this.loadBlockchainData()
}
async loadBlockchainData() {
const web3 = window.web3
const accounts = await web3.eth.getAccounts()
this.setState({ account: accounts[0] })
const networkId = await web3.eth.net.getId()
// Load DaiToken
const daiTokenData = DaiToken.networks[networkId]
if(daiTokenData) {
const daiToken = new web3.eth.Contract(DaiToken.abi, daiTokenData.address)
this.setState({ daiToken })
let daiTokenBalance = await daiToken.methods.balanceOf(this.state.account).call()
this.setState({ daiTokenBalance: daiTokenBalance.toString() })
} else {
window.alert('DaiToken contract not deployed to detected network.')
}
// Load TokenFarm
const tokenFarmData = TokenFarm.networks[networkId]
if(tokenFarmData) {
const tokenFarm = new web3.eth.Contract(TokenFarm.abi, tokenFarmData.address)
this.setState({ tokenFarm })
let donatingBalance = await tokenFarm.methods.donatingBalance(this.state.account).call()
this.setState({ donatingBalance: donatingBalance.toString() })
} else {
window.alert('TokenFarm contract not deployed to detected network.')
}
}
async loadWeb3() {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
await window.ethereum.enable()
}
else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
}
else {
window.alert('Non-Ethereum browser detected. You should consider trying MetaMask!')
}
}
donateTokens = (amount) => {
// sometimes this.state call is not working (if you e.g. didn't build a project), and you should try to
// hard-code an 0x addresses here (not recommended)
this.state.daiToken.methods.approve(this.state.tokenFarm._address, amount).send({ from: this.state.account }).on('transactionHash', (hash) => {
this.state.tokenFarm.methods.donateTokens(amount).send({ from: this.state.account }).on('transactionHash', (hash) => {
})
})
}
constructor(props) {
super(props)
this.state = {
account: '0x0',
daiToken: {},
dappToken: {},
tokenFarm: {},
daiTokenBalance: '0',
donatingBalance: '0'
}
}
render() {
let content
content = <Main
daiTokenBalance={this.state.daiTokenBalance}
donatingBalance={this.state.donatingBalance}
donateTokens={this.donateTokens}
/>
return (
<div>
<Navbar account={this.state.account} />
<div className="container-fluid mt-5">
<div className="row">
<main role="main" className="col-lg-12 ml-auto mr-auto" style={{ maxWidth: '600px' }}>
<div className="content mr-auto ml-auto">
<a
href="<some URL>"
target="_blank"
rel="noopener noreferrer"
>
</a>
{content}
</div>
</main>
</div>
</div>
</div>
);
}
}
export default App;
The index.js
file serves to call selected functions.
Here is the code:
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css'
import App from './components/App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
There is one more step to be able to run this project, and that is to integrate Ganache with MetaMask. This enables you to select an account via MetaMask, from Ganache.
The following image shows the main GUI of Ganache:
Here, the RPC Server parameter is important. RPC stands for Remote Procedure Call, which is a request-response protocol. We have configured the host (127.0.0.1
), and the port (7545
) in the truffle-config.js
file.
We will create a custom RPC network in MetaMask, based on these parameters:
Now you have created a custom RPC network.
The next step is to import some accounts from Ganache to MetaMask. For this, you will need a private key of an account:
Now that everything is set, you can run the project:
npm start
localhost:<port>
Note that it should automatically open the project in a browser, without an explicit call.
In this article, we talked about Ethereum decentralized applications (DApps), theory and implementation-wise.
In the DApps theory section, we've seen the following:
In the DApps practical section, we've seen the following:
Nemanja Grubor
See other articles by Nemanja
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!