Last summer, my wife and I watched a program called “StartUp” on Netflix. The three-season series features the challenges faced by a cast of characters to create “GenCoin,” a new (and fictional) digital currency. As I watched the show, I wondered how my life would be different if I were a member of the engineering team driving GenCoin adoption.
While products like GenCoin originated with the purpose of providing a new way to perform financial transactions, the underlying concepts are more powerful than just currency-related tasks. In fact, the distributed nature in how blockchain is designed ties directly to the heart of Web3.
Before going any further, let’s recap the three main eras of the web:
Web3 provides an alternative for the web2 reality where control is centralized into a handful of technology providers such as Google, Apple, and Amazon. Web3 creates a permissionless datastore where no one person or corporation controls or owns the data, yet that data is still guaranteed to be true. The data is stored on the public ledgers of a blockchain network. So instead of one entity owning the data, multiple nodes (computers running the blockchain) store the data and come to a consensus about whether the data is valid or not.
The protocol to create such a datastore (which began with Bitcoin and continued with protocols such as Ethereum and others) is the foundation of web3 and opens a wide variety of new use cases, such as:
The key to all of the above, of course, is that the ownership of the digital currency—DAO membership, music rights, and so on—is in the hands of the user and controlled by the user. Anyone, anywhere in the world with an internet connection can freely trade, sell, and build upon those items. There is no centralized company or government in control and making the rules.
How close to this ideal web3 has come or can come—and if it’s a good thing or not—is an in-depth conversation with many strong (and strongly biased) opinions. I won’t get into that here. Something else to mention is that web3 is not expected to replace web2, in the same manner by which web2 did not replace web1. All three will have their place in the future.
Let’s put debate aside and instead look at how you, as a developer, can explore the technical side of this new web3 world.
The Full-Stack Developer term gained momentum around 2015, offering the benefit that a single software engineer could contribute to any level of the software stack. As a result, if a feature or bug related to the service tier was logged, the same developer who just finished a client-related task could pick up the ticket and be productive.
Despite one’s opinion of full-stack developers, the focus now should shift to thinking about what the web3 stack looks like and how software engineers will introduce features and functionality in this next generation of web development.
Diving into web3 in detail would involve a lengthy series of articles. We’re going to keep it high level and provide an overview with some links to additional resources. We’ll look at the typical first steps: creating a smart contract and then a Dapp to interact with that smart contract.
A smart contract is a piece of code deployed on the blockchain (in our case below, on Ethereum). This code is immutable and permissionless. Once you deploy it, it exists on the blockchain, can’t be changed, and anyone can retrieve it.
A dapp (decentralized application) is how we will interact with that smart contract from our UI (typically a web page or app). A dapp is utilizing the open nature of smart contracts on the back end. It can also be stored in a decentralized file storage such as IPFS (InterPlanetary File Storage) so there is no chance of downtime. DDOS attacks are also extremely difficult to execute because you would have to attack every single node the site is stored on.
Conversely, security and testing is much more important though. Flaws and vulnerabilities in the code need to be ironed out before deployment is considered.
Let’s walk through this in more detail.
A combination of proven technologies and purpose-driven technologies exists for software engineers seeking to dive into the web3 end of the pool. One very popular stack includes the following components:
Let’s assume a homeowner’s association is about to host their periodic elections, where residents in the neighborhood vote on a series of decisions. Since the neighborhood contains a group of IT professionals eager to become web3 pioneers, they decide to build an Ethereum Dapp for the election.
Why would this be a good example?
Using a Dapp to store the votes provides results that are indisputable. If set up correctly, the ability to tamper or fake votes is non-existent because data is stored publicly on the blockchain rather than on the private server of a single company. Anyone could retrieve the vote results in a permission-less manner by interacting with the smart contract.
The first step is to create our Smart Contract for use with the application, which utilizes the following components of the web3 stack:
The creation of a Smart Contract follows a flow similar to the illustration below:
This flow has been fully detailed by the team at ConsenSys:
With the Smart Contact in place, web3 engineers can focus on building the application which will be used by the association’s election. The following web3 stack components remain from the list above:
For this example, we are going to leverage the React framework, which will utilize the following flow:
Now, let’s build something from the ground up.
After creating a free account at Infura, I created a new project called jvc-homeowners-ballot
:
The new project contains the following details, which I will reference later:
On my local machine, I created a matching folder, called jvc-homeowners-ballot
and then initialized Truffle using the following CLI command:
truffle init
The initialization results in the following directory structure:
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── test
└── truffle-config.js
The Truffle-based wallet provider dependency was added next:
npm install --save @truffle/hdwallet-provider
In order to create a local development network, the Ganache CLI was launched using the following command:
ganache
The CLI responded with the following information and Ganache is now running on port #8545 of my local machine:
ganache v7.0.1 (@ganache/cli: 0.1.2, @ganache/core: 0.1.2)
Starting RPC server
Available Accounts
==================
(0) 0x2B475e4fd7F600fF1eBC7B9457a5b58469b9EDDb (1000 ETH)
(1) 0x5D4BB40f6fAc40371eF1C9B90E78F82F6df33977 (1000 ETH)
(2) 0xFaab2689Dbf8b7354DaA7A4239bF7dE2D97e3A22 (1000 ETH)
(3) 0x8940fcaa55D5580Ac82b790F08500741326836e0 (1000 ETH)
(4) 0x4c7a1b7EB717F98Fb0c430eB763c3BB9212F49ad (1000 ETH)
(5) 0x22dFCd5df8d4B19a42cB14E87219fea7bcA7C92D (1000 ETH)
(6) 0x56882f79ecBc2D68947C6936D4571f547890D07c (1000 ETH)
(7) 0xD257AFd8958c6616bf1e61f99B2c65dfd9fEE95A (1000 ETH)
(8) 0x4Bb2EE0866578465E3a2d3eCCC41Ea2313372B20 (1000 ETH)
(9) 0xdf267AeFeAfE4b7053ca10c3d661a8CB24E98236 (1000 ETH)
Private Keys
==================
(0) 0x5d58d27b0f294e3222bbd99a3a1f07a441ea4873de6c3a2b7c40b73186eb616d
(1) 0xb9e52d6cfb2c074fa6a6578b946e3d00ea2a332bb356d0b3198ccf909a97fdc8
(2) 0xc52292ce17633fe2724771e81b3b4015374d2a2ea478891dab74f2028184edeb
(3) 0xbc7b0b4581592e48ffb4f6420228fd6b3f954ac8cfef778c2a81188415274275
(4) 0xc63310ccdd9b8c2da6d80c886bef4077359bb97e435fb4fe83fcbec529a536fc
(5) 0x90bc16b1520b66a02835530020e43048198195239ac9880b940d7b2a48b0b32c
(6) 0x4fb227297dafb879e148d44cf4872611819412cdd1620ad028ec7c189a53e973
(7) 0xf0d4dbe2f9970991ccc94a137cfa7cf284c09d0838db0ce25e76c9ab9f4316d9
(8) 0x495fbc6a16ade5647d82c6ad12821667f95d8b3c376dc290ef86c0d926f50fea
(9) 0x434f5618a3343c5e3b0b4dbeaf3f41c62777d91c3314b83f74e194be6c09416b
HD Wallet
==================
Mnemonic: immense salmon nominee toy jungle main lion universe seminar output oppose hungry
Base HD Path: m/44'/60'/0'/0/{account_index}
Default Gas Price
==================
2000000000
BlockGas Limit
==================
30000000
Call Gas Limit
==================
50000000
Chain Id
==================
1337
RPC Listening on 127.0.0.1:8545
Within my project folder, the truffle-config.js
file was updated to activate the following lines:
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
Now, the Truffle console can be launched in a new terminal window:
truffle console
… which results in a simple console:
truffle(development)>
The console can be used to create a wallet:
const HDWalletProvider = require('@truffle/hdwallet-provider');
This should result in a response of undefined. This is okay.
Next, we need a 12-word mnemonic phrase, so I used the Mnemonic Code Converter site to generate one.
I then used that 12-word phrase to update the Truffle console:
const mnemonic = '12 words here';
const wallet = new HDWalletProvider(mnemonic, "http://localhost:8545");
Both of these also resulted in a response of undefined, but the wallet console provided results that the commands actually worked, as shown below:
truffle(development)> wallet
HDWalletProvider {
walletHdpath: "m/44'/60'/0'/0/",
wallets: {
...
},
addresses: [
'0xa54b012b406c01dd99a6b18ef8b55a15681449af',
'0x6d507a70924ea3393ae1667fa88801650b9964ad',
'0x1237e0a8522a17e29044cde69b7b10b112544b0b',
'0x80b4adb18698cd47257be881684fff1e14836b4b',
'0x09867536371e43317081bed18203df4ca5f0490d',
'0x89f1eeb95b7a659d4748621c8bdbabc33ac47bbb',
'0x54ceb6f0d722dcb33152c953d5758a08045f254d',
'0x25d2a8716792b98bf9cce5781b712f00cf33227e',
'0x37b6364fb97028830bfeb0cb8d2b14e95e2efa05',
'0xe9f56031cb6208ddefcd3cdd5a1a41f7f3400af5'
],
...
Now we need to acquire some test funds for our Dapp and will use Ropsten Ethereum Faucet to add funds to my existing MetaMask wallet, created by ConsenSys. Keep in mind, you can create multiple accounts in MetaMask, where at least one account is dedicated for development and testing. Doing so reduces the risk of accidentally losing real funds. Also, never share your seed phrase with anyone and never upload your private key … anywhere!
To add some test funds, I only had to include my account address:
Using the Ropsten Etherscan site, we can validate the transaction completed successfully:
The dotenv
dependency was added to the project using the following command:
npm install --save dotenv
Next, a new file called .env
was created at the root of the project and contained the following two lines:
INFURA_API_KEY=INSERT YOUR API KEY HERE (no quotations)
MNEMONIC="12 words here"
The INFURA_API_KEY
is the Project ID that was given when the jvc-homeowners-ballot
project was created.
Important note: Make sure the .env file is included in the .gitignore
file to avoid this secret information from being available to others with access to the repository.
The last preparation step is to update the truffle-config.js
file. First, we need to add the following lines at the top of the file:
require("dotenv").config();
const HDWalletProvider = require("@truffle/hdwallet-provider");
Next, we need to add the following network, which will leverage the dotenv
dependency added above:
ropsten: {
provider: () =>
new HDWalletProvider(
process.env.MNEMONIC,
`https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`
),
network_id: 3, // Ropsten's id
gas: 5500000, // Ropsten has a lower block limit than mainnet
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
With Infura and Truffle in place and some test funds in our account, it’s time to focus on our smart contract.
For the homeowner’s association election, we will use the following contract, which is called JvcHomeownerBallot.sol
and located in the contracts folder of the project:
// SPDX-License-Identifier: UNLICENSED (it is common practice to include an open source license or declare it unlicensed)
pragma solidity ^0.8.7; // tells the compiler which version to use
contract Homeowners {
// store the addresses of voters on the blockchain in these 2 arrays
address[] votedYes;
address[] votedNo;
function voteYes() public {
votedYes.push(msg.sender);
}
function voteNo() public {
votedNo.push(msg.sender);
}
function getYesVotes() public view returns (uint) {
return votedYes.length;
}
function getNoVotes() public view returns (uint) {
return votedNo.length;
}
}
For this example our contract will be quite simple, where voters can either select Yes or No for whatever question is on the ballot.
At this point the contracts folder appears as shown below:
.
├── JvcHomeownersBallot.sol
└── Migrations.sol
With the contract in place, we need to establish a way to deploy the contract. This is where the migrations folder comes into place. The following contents were added to a 2_deploy_contracts.js
file inside the migrations
folder:
const JvcHomeownersBallot = artifacts.require("JvcHomeownersBallot.sol");
module.exports = function(deployer) {
deployer.deploy(JvcHomeownersBallot);
};
Now, we can perform the migration of the contract using the following command:
truffle migrate --network ropsten
The migrate keyword provides the following response:
Compiling your contracts...
===========================
> Compiling ./contracts/JvcHomeownersBallot.sol
> Artifacts written to /Users/john.vester/projects/jvc/consensys/jvc-homeowners-ballot/build/contracts
> Compiled successfully using:
- solc: 0.8.11+commit.d7f03943.Emscripten.clang
Network up to date.
truffle(development)> truffle migrate --network ropsten
Compiling your contracts...
===========================
> Compiling ./contracts/JvcHomeownersBallot.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /Users/john.vester/projects/jvc/consensys/jvc-homeowners-ballot/build/contracts
> Compiled successfully using:
- solc: 0.8.11+commit.d7f03943.Emscripten.clang
Starting migrations...
======================
> Network name: 'ropsten'
> Network id: 3
> Block gas limit: 8000000 (0x7a1200)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0x5f227f26a31a3667a689be2d7fa6121a21153eb219873f6fc9aecede221b3b82
> Blocks: 5 Seconds: 168
> contract address: 0x9e6008B354ba4b9f91ce7b8D95DBC6130324024f
> block number: 11879583
> block timestamp: 1643257600
> account: 0xa54b012B406C01dd99A6B18eF8b55A15681449Af
> balance: 1.573649230299520359
> gas used: 250142 (0x3d11e)
> gas price: 2.506517682 gwei
> value sent: 0 ETH
> total cost: 0.000626985346010844 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 11879584)
> confirmation number: 2 (block: 11879585)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.000626985346010844 ETH
2_deploy_contracts.js
=====================
Deploying 'JvcHomeownersBallot'
-------------------------------
> transaction hash: 0x1bf86b0eddf625366f65a996e633db589cfcef1a4d6a4d6c92a5c1f4e63c767f
> Blocks: 0 Seconds: 16
> contract address: 0xdeCef6474c95E5ef3EFD313f617Ccb126236910e
> block number: 11879590
> block timestamp: 1643257803
> account: 0xa54b012B406C01dd99A6B18eF8b55A15681449Af
> balance: 1.573133154908720216
> gas used: 159895 (0x27097)
> gas price: 2.507502486 gwei
> value sent: 0 ETH
> total cost: 0.00040093710999897 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 11879591)
> confirmation number: 2 (block: 11879592)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00040093710999897 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.001027922456009814 ETH
- Blocks: 0 Seconds: 0
- Saving migration to chain.
- Blocks: 0 Seconds: 0
- Saving migration to chain.
At this point we have deployed the JvcHomeownersBallot
smart contract to the Ropsten network. The smart contract can be verified using the following URL and providing the contract address in the “Deploying JvcHomeownersBallot” logs:
Or … in this case:
https://ropsten.etherscan.io/address/0xdeCef6474c95E5ef3EFD313f617Ccb126236910e
For the prior steps, I used a folder called jvc-homeowners-ballot
. On that same level, I will create a React application called jvc-homeowners-ballot-client
using the React CLI:
npx create-react-app jvc-homeowners-ballot-client
Next, I changed directories into the newly-created folder and executed the following to install the web3
dependency into the React application:
cd jvc-homeowners-ballot-client
npm install web3
With the core React application ready, a contract application binary interface (ABI) needs to be established to allow our Dapp to communicate with contracts on the Ethereum ecosystem. Based upon the contents of the JvcHomeownerBallot.sol
smart contract file, I navigated to the build/contracts
folder and opened the JvcHomeownersBallet.json
file and used the values for the “abi” property for the jvcHomeOwnersBallot
constant of the abi.js
file as shown below:
export const jvcHomeownersBallot = [
{
"inputs": [],
"name": "voteYes",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "voteNo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getYesVotes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "getNoVotes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
];
This file was placed into a newly-created abi
folder inside the src
folder of the React application.
Now, the React Apps.js
file needs to be updated. Let’s first start with the top of the file, which needs to be configured as shown below:
import React, { useState } from "react";
import { jvcHomeownersBallot } from "./abi/abi";
import Web3 from "web3";
import "./App.css";
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "0xdeCef6474c95E5ef3EFD313f617Ccb126236910e";
const storageContract = new web3.eth.Contract(jvcHomeownersBallot, contractAddress);
The contactAddress
can be found a number of ways. In this case, I used the results in the truffle - migrate CLI command. Another option is to use the Etherscan site.
At this point, standard React development can take over. The finished App.js
file will look like this:
import React, { useState } from "react";
import { jvcHomeownersBallot } from "./abi/abi";
import Web3 from "web3";
import Nav from "./components/Nav.js";
import "./App.css";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import {CircularProgress, Grid, Typography} from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
}));
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "0xdeCef6474c95E5ef3EFD313f617Ccb126236910e";
const storageContract = new web3.eth.Contract(jvcHomeownersBallot, contractAddress);
function App() {
const classes = useStyles();
const [voteSubmitted, setVoteSubmitted] = useState("");
const [yesVotes, setYesVotes] = useState(0);
const [noVotes, setNoVotes] = useState(0);
const [waiting, setWaiting] = useState(false);
const getVotes = async () => {
const postYes = await storageContract.methods.getYesVotes().call();
setYesVotes(postYes);
const postNo = await storageContract.methods.getNoVotes().call();
setNoVotes(postNo);
};
const voteYes = async () => {
setWaiting(true);
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = (await storageContract.methods.voteYes().estimateGas()) * 1.5;
const post = await storageContract.methods.voteYes().send({
from: account,
gas,
});
setVoteSubmitted(post.from);
setWaiting(false);
};
const voteNo = async () => {
setWaiting(true);
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = (await storageContract.methods.voteNo().estimateGas() * 1.5);
const post = await storageContract.methods.voteNo().send({
from: account,
gas,
});
setVoteSubmitted(post.from);
setWaiting(false);
};
return (
<div className={classes.root}>
<Nav ></Nav>
<div className="main">
<div className="card">
<Typography variant="h3" gutterBottom>
JVC Homeowners Ballot
</Typography>
<Typography gutterBottom>
How do you wish to vote?
</Typography>
<span className="buttonSpan">
<Button
id="yesButton"
className="button"
variant="contained"
color="primary"
type="button"
onClick={voteYes}>Vote Yes</Button>
<div className="divider"></div>
<Button
id="noButton"
className="button"
color="secondary"
variant="contained"
type="button"
onClick={voteNo}>Vote No</Button>
<div className="divider"></div>
</span>
{waiting && (
<div>
<CircularProgress ></CircularProgress>
<Typography gutterBottom>
Submitting Vote ... please wait
</Typography>
</div>
)}
{!waiting && voteSubmitted && (
<Typography gutterBottom>
Vote Submitted: {voteSubmitted}
</Typography>
)}
<span className="buttonSpan">
<Button
id="getVotesButton"
className="button"
color="default"
variant="contained"
type="button"
onClick={getVotes}>Get Votes</Button>
</span>
{(yesVotes > 0 || noVotes > 0) && (
<div>
<Typography variant="h5" gutterBottom>
Current Results
</Typography>
<Grid container spacing={1}>
<Grid item xs={6}>
<div className="resultsAnswer resultsHeader">Vote</div>
</Grid>
<Grid item xs={6}>
<div className="resultsValue resultsHeader"># of Votes</div>
</Grid>
<Grid item xs={6}>
<div className="resultsAnswer">Yes</div>
</Grid>
<Grid item xs={6}>
<div className="resultsValue">{yesVotes}</div>
</Grid>
<Grid item xs={6}>
<div className="resultsAnswer">No</div>
</Grid>
<Grid item xs={6}>
<div className="resultsValue">{noVotes}</div>
</Grid>
</Grid>
</div>
)}
</div>
</div>
</div>
);
}
export default App;
To start the React-based Dapp, the Yarn CLI can be used:
yarn start
Once compiled and validated, the application will appear on the screen, as shown below:
At this point, three options are available:
After voting YES the first time, I created the following video to submit a NO vote and then use the GET VOTES button:
This video can also be found on .
With the Smart Contract established, the remaining tasks for the web3 pioneer are not considerably different from a client perspective:
Where web3 applications differ from their predecessors is that it doesn’t matter who is seeking information stored within the smart contract within the blockchain. The answer is always the same, providing a single source of truth when making requests for information.
In the simple use case of the homeowner’s association election, no matter how many times the ballots are queried, the results will always be exactly the same—even if a second Dapp is written to access the same data.
Since last year, I have been trying to live by the following mission statement, which I feel can apply to any IT professional:
“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.”
- J. Vester
Web3 Dapps certainly adhere to my personal mission statement on multiple levels:
Full-stack developers seeking to become web3 pioneers have a collection of tooling without a hefty learning curve. The frameworks and libraries can help software engineers explore and design against the next generation of web development.
If you are interested in the source code used by this project, both repositories are available on GitLab:
https://gitlab.com/johnjvester/jvc-homeowners-ballot
Have a really great day!
Also published on: https://dzone.com/articles/moving-from-full-stack-developer-to-web3-pioneer