The application we are building is a survey application where users receive cryptocurrency in return for their participation. This will be a truncated version of the actual application, but enough so that you can get a basic introduction to smart contracts.
The broad tech stack for this application is as follows: JavaScript (ReactJS, NodeJS, ExpressJS), SQL and Solidity. It also makes use of a JavaScript library called SurveyJS. Other tools were used, but for now we will just focus on the bare bones.
Step 1: Create React App
Open a terminal (preferably already within an IDE) and enter:
npx create-react-app myWeb3Dapp
myWeb3Dapp
is the name of the app's root folder. Once you have done this, remove all of the files except for App.css
, App.js
, index.js
and index.css
Step 2: Setup directories and install tools
I like to organise the folder structure in advance where possible as it helps to give a clear minded view of the application's architecture.
Since we are also going to be using Hardhat
as our Ethereum development environment, now is a good time to install that too. Hardhat will create a mock Ethereum blockchain upon which we can simulate transactions.
First, head to the root directory and enter:
npm install --save-dev hardhat
We can also set up our Ethereum environment here by entering:
npx hardhat
then choose:
Create a basic sample project
This will create a file called hardhat.config.js
and create two new folders in your root directory:
scripts
contracts
Open hardhat.config.js
, delete what exists and update it with the following (Your Solidity version might be higher than this by the time you read this):
require("@nomiclabs/hardhat-waffle");
const {task} = require("hardhat/config");
require("dotenv").config()
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
module.exports = {
paths: {
artifacts: "./src/artifacts",
},
networks: {
hardhat: {
chainId: 1337
},
},
solidity: {
version: "0.8.6",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
};
Next we need to install two more libraries which are called by require
in the file above:
npm install @nomiclabs/hardhat-waffle dotenv
Next we need to install ethers
which is a JavaScript library that will allow our application to communication with the Ethereum blockchain.
npm install ethers
Step 3: Write smart contracts
In this section we will build the smart contract using Solidity. We need to write two smart contracts - one which represents our 'CTK' token and another which represents the 'Owner' of the tokens, which effectively plays the role of a bank in that it holds and releases the tokens subject to approval.
Navigate to the contracts
folder, delete the file called greeter.sol
and create two new files:
touch CryptocracyToken.sol Owner.sol
Inside CryptocracyToken.sol update the code with the following:
pragma solidity ^0.8.6;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CryptocracyToken is IERC20, ERC20 {
constructor(address holder, string memory name, string memory symbol)
public ERC20(name, symbol) {
_mint(holder, 100000 * (10 ** 18));
}
}
Inside Owner.sol update the code with the following:
pragma solidity ^0.8.6;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Owner {
function approveTokenSpend(IERC20 token, address spender, uint256 amount)
public {
token.approve(spender, amount);
}
function withdrawToken(IERC20 token, address recipient, uint256 amount)
public {
token.transfer(msg.sender, amount);
}
}
Step 4: Front-end
Create client directories
Navigate to the src
folder and create three directories:
mkdir
components
pages
stylesheets
Create homepage file
Navigate to the pages
folder and type the following to create your homepage file.
touch home-page.js
Open the file and update it with the following code:
import React from "react";
import HomePageHero from "../components/home-page-hero";
import Navbar from "../components/navbar";
const HomePage = () => {
return (
<>
<Navbar/>
<HomePageHero/>
</>
);
}
export default HomePage
As you can see, our home page will consist of two components. Hypothetically we can reuse these components anywhere else on our site.
Create our first two components
Navigate to the components
directory and create two new files:
touch navbar.js home-page-hero.js global-button.js
Update them with the following code (ignore any errors for now):
Navbar:
import {useEffect, useState} from "react";
import {Link} from "react-router-dom";
import '../stylesheets/navbar.css'
const Navbar = () => {
const [clicker, setClicker] = useState(false);
const [button, setButton] = useState(true);
const handleClick = () => setClicker(!clicker);
const closeMobileMenu = () => setClicker(false);
const showButton = () => {
if (window.innerWidth <= 960) {
setButton(false);
} else {
setButton(true);
}
};
useEffect(() => {
showButton();
}, []);
window.addEventListener("resize", showButton);
return (
<>
<nav className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo"
onClick={closeMobileMenu}>Cryptocracy</Link>
<div className="menu-icon" onClick={handleClick}>
<i className={clicker ? "fas fa-times" : "fas" +
" fa-bars"}/>
</div>
</div>
</nav>
</>
);
}
export default Navbar
HomePageHero
import {Link} from "react-router-dom";
import {GlobalButton} from "./global-button";
import '../stylesheets/home-page-hero.css'
const HomePageHero = () => {
return (
<div>
<div className="hero-container">
<div className="title-container">
<h2>We dont just value your opinion, we reward it.</h2>
</div>
<div className="subtitle-container">
<p>Complete Surveys, Quizzes and Polls for Crypto Rewards</p>
</div>
<div className="hero-btns">
<GlobalButton className="btns"
buttonStyle="btn--outline"
buttonSize="btn--large">
<Link to="/surveys">Earn Crypto Now!</Link>
</GlobalButton>
</div>
</div>
</div>
);
}
export default HomePageHero
GlobalButton
import React from "react";
import { Link } from "react-router-dom";
import '../stylesheets/global-button.css'
const STYLES = ["btn--primary", "btn--outline"];
const SIZES = ["btn--medium", "btn--large"];
const GlobalButton = ({children, type, onClick, buttonStyle, buttonSize}) => {
const checkButtonStyle = STYLES.includes(buttonStyle) ? buttonStyle : STYLES[0];
const checkButtonSize = SIZES.includes(buttonSize) ? buttonSize : SIZES[0]
return (
<Link to="#" className="btn-mobile">
<button
className={`btn ${checkButtonStyle} ${checkButtonSize}`}
onClick={onClick}
type={type}
>
{children}
</button>
</Link>
)
};
export default GlobalButton
then navigate to the stylesheets folder and type the following into the terminal
touch home-page-hero.css navbar.css global-button.css
and update them with the following:
home-page-hero.css
.hero-container {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
#box-shadow: inset 0 0 0 1000px rgba(205, 168, 168, 0.2);
background-color: #ffffff;
object-fit: contain;
}
.title-container > h2 {
color: #000000;
font-size: 3rem;
font-family: 'Arvo', serif;
font-weight: bold;
}
.title-container {
display: flex;
margin-top: -20vh!important;
margin-bottom: 0!important;
max-width: 50vw;
text-align: center;
}
.subtitle-container > p {
font-size: 1.7rem;
font-family: 'Arvo', serif;
font-weight: bold;
color: #000000;
text-align: center;
}
.hero-btns {
margin-top: 32px;
}
.btn--outline {
border: 1px solid #5b3926;
}
.btn--primary {
border: 1px solid #5b3926;
#background-color: #fff8ee;
}
.btn--medium {
border: 1px solid #5b3926;
color: #000000;
}
#btn-logout {
display: none;
}
#btn-login {
display: none;
}
a {
text-decoration: none;
color: #000000;
}
@media screen and (max-width: 991px) {
.hero-container > h1 {
font-size: 70px;
margin-top: -150px;
}
}
@media screen and (max-width: 768px) {
.hero-container > h1 {
font-size: 50px;
margin-top: -100px;
}
.hero-container > p {
font-size: 30px;
}
.btn-mobile {
display: block;
text-decoration: none;
}
.btn {
width: 100%;
}
}
navbar.css
.navbar {
#background: linear-gradient(90deg, rgb(28, 27, 27) 0%, rgb(26, 23, 23) 100%);
background-color: #ffffff;
height: 100px;
display: flex;
justify-content: center!important;
align-items: center;
font-size: 1.2rem;
position: sticky;
top: 0;
z-index: 999;
width: 100%;
#box-shadow: 2px 2px 6px 0px rgba(0, 0, 0, 0.3);
}
.navbar-container {
display: flex;
height: 80px;
#width: 100%;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
}
.navbar-logo {
color: #000000;
font-family: 'Libre Barcode 39 Text', cursive;
cursor: pointer;
text-decoration: none;
font-size: 4rem;
display: flex;
align-items: flex-start;
}
.nav-menu {
display: grid;
grid-template-columns: repeat(4, auto);
grid-gap: 10px;
list-style: none;
text-align: center;
width: 60vw;
justify-content: end;
margin-right: 2rem;
}
.nav-links {
color: #000000;
display: flex;
align-items: center;
text-decoration: none;
padding: 0.5rem 1rem;
height: 100%;
font-size: 1.4rem;
}
.nav-links:hover {
border-bottom: 4px solid #000000;
transition: all 0.2s ease-out;
}
.fa-bars {
color: #000000;
}
.nav-links-mobile {
display: none;
}
.menu-icon {
display: none;
}
@media screen and (max-width: 960px) {
.NavbarItems {
position: relative;
}
.nav-menu {
display: flex;
flex-direction: column;
width: 100%;
height: 90vh;
position: absolute;
top: 80px;
left: -100%;
opacity: 1;
transition: all 0.5s ease;
}
.nav-menu.active {
background: #242222;
left: 0;
opacity: 1;
transition: all 0.5s ease;
z-index: 1;
}
.nav-links {
text-align: center;
padding: 2rem;
width: 100%;
display: table;
}
.nav-links:hover {
background-color: #fff;
color: #242424;
border-radius: 0;
}
.navbar-logo {
position: absolute;
top: 0;
left: 0;
transform: translate(25%, 50%);
}
.menu-icon {
display: block;
position: absolute;
top: 0;
right: 0;
transform: translate(-100%, 60%);
font-size: 1.8rem;
cursor: pointer;
}
.fa-times {
color: #fff;
font-size: 2rem;
}
.nav-links-mobile {
display: block;
text-align: center;
margin: 2rem auto;
border-radius: 4px;
width: 80%;
text-decoration: none;
font-size: 1.5rem;
background-color: transparent;
color: #ec0000;
padding: 14px 20px;
border: 1px solid #fff;
transition: all 0.3s ease-out;
}
.nav-links-mobile:hover {
background: #fff;
color: #c94444;
transition: 250ms;
}
}
global-button.css
:root {
--primary: #fff;
}
.btn {
padding: 8px 20px;
border-radius: 2px;
#outline: none;
#border: none;
cursor: pointer;
}
.btn--primary {
color: #242424;
border: 1px solid #000000;
}
.btn--outline {
background-color: transparent;
color: #000000;
padding: 8px 20px;
border: 1px solid #000000;
transition: all 0.3s ease-out;
}
.btn--medium {
padding: 8px 20px;
font-size: 20px;
}
.btn--large {
padding: 8px 20px;
font-size: 20px;
}
.btn--medium:hover, .btn--large:hover {
background-color: #fff;
color: #242424;
transition: all 0.3s ease-out;
}
Setting up the App.js file**
Delete everything in the App.js
file and replace it with the code below. We will be updating this file throughout so keep it open.
import React, {useMemo, useState} from "react";
import {HashRouter as Router, Routes, Route} from "react-router-dom";
import './App.css'
import HomePage from './pages/home-page'
const App = () => {
return(
<>
<Router>
<Routes>
<Route exact path='/' element={<HomePage/>} replace/>
</Routes>
</Router>
</>
)
}
export default App
Also, go to index.js
and remove the following line:
import reportWebVitals from './reportWebVitals';
Then, in the terminal, run
npm start
You should see this: It doesn't look like much, but remember most of what we have done so far is the behind the scenes work.
Create the dashboard
Navigate to the pages
folder and create a new file:
touch dashboard-page.js
update this file with the following code:
import Navbar from "./components/navbar";
export default function DashboardPage(){
return (
<>
<Navbar/>
<DashboardPageHero/>
</>
);
}
then navigate to the components
folder and create a new file:
touch dashboard-page-hero.js
Update that file with the following code:
import image from '../surveytilecover.gif'
export default function DashboardPageHero() {
return (
<>
<div className="dashboardPageContainer">
<div className="titleContainer">
<h1>Surveys available</h1>
</div>
<div className="surveyContainer">
<CardItem src={image}
id="surveys"
text="Which party will you vote for? (50 CTK)"
label="Politics"
path="/survey"
/>
</div>
</div>
</>
);
}
Also, add the following line to App.js
:
<Route exact path='/surveys' element={<DashboardPage/>} replace/>
So App.js
would now look like the following:
import {HashRouter as Router, Routes, Route} from "react-router-dom";
import './App.css'
import HomePage from './pages/home-page'
const App = () => {
return(
<>
<Router>
<Routes>
<Route exact path='/' element={<HomePage/>} replace/>
<Route exact path='/surveys' element={<DashboardPage/>} replace/>
</Routes>
</Router>
</>
)
}
export default App
then create a new file in the components
folder called card-item
which will represent our survey tile on the dashboard page hero:
touch card-item.js
which you should update with this code:
import { Link } from "react-router-dom"
export default function CardItem(props) {
return (
<>
<div className="cards__item">
<Link className="cards__item___link" to={props.path}>
<figure className="cards__item___pic-wrap" data-category={props.label}>
<img
alt="DemocracyImage"
className="cards__item__img"
src={props.src}
/>
</figure>
<div className="cards__item__info">
<h5 className="cards__item__text">{props.text}</h5>
</div>
</Link>
</div>
</>
);
}
Adding a survey
As mentioned earlier, we will be using JavaScript library SurveyJS to add a survey to our app.
Install Survey-React
npm install survey-react
Create a new folder in the client folder:
mkdir surveys
Navigate to this folder and create two files:
touch survey-control.js survey-questions.js
Update each with the following code:
survey-control.js
import React, {useCallback, useState} from "react";
import {Link} from "react-router-dom";
import * as Survey from "survey-react";
import {GlobalButton} from "../components/global-button";
import {SurveyQuestions} from "./survey-questions"
export const SurveyControl = () => {
const [showPage, setShowPage] = useState(true);
const OnCompletePage = useCallback(() => {
setShowPage(!showPage);
}, [showPage]);
const SetFinalPage = ({}) => {
return (
<main>
<h1>Thank you for taking this survey. You have earned 50 CTK!</h1>
<GlobalButton
className="btns"
buttonStyle="btn--primary"
buttonSize="btn--large">
<Link to="/surveys">Back to Dashboard</Link>
</GlobalButton>
</main>
);
};
const survey = new Survey.Model(SurveyQuestions);
return (
<div>{
showPage ?
<Survey.Survey
showCompletedPage={false}
onComplete={OnCompletePage}
model={survey}
/>
: <SetFinalPage/>
}</div>
);
};
survey-questions.js
import React from "react";
import * as Survey from "survey-react";
Survey.StylesManager.applyTheme("modern");
export const SurveyQuestions = {
"pages": [
{
"elements": [
{
"type": "radiogroup",
"name": "Party I am most likely to vote for",
"title": "Please select the political party youre most likely to vote for",
"isRequired": true,
"hasNone": true,
"colCount": 1,
"choices": [
"Red Party",
"Blue Party",
"Yellow Party",
"Green Party",
"Orange Party"
]
}
]
}
],
"showTitle": false,
"isAllRowRequired": true,
};
The survey should look something like this:
You should also update App.js at this point with the following code:
import React from "react";
import {HashRouter as Router, Routes, Route} from "react-router-dom";
import './App.css'
import HomePage from './pages/home-page'
import DashboardPage from "./pages/dashboard-page";
import {SurveyControl} from "./surveys/survey-control";
const App = () => {
return(
<>
<Router>
<Routes>
<Route exact path='/' element={<HomePage/>} replace/>
<Route exact path='/surveys' element={<DashboardPage/>} replace/>
<Route exact path='/survey' element={<SurveyControl/>} replace/>
</Routes>
</Router>
</>
)
}
export default App
We now have a basic front end set up with the ability to complete a survey. The next step is connecting to the Ethereum blockchain and claiming your hard earned crypto tokens.
To do this, we will use a tool called Hardhat and a JavaScript library called Ethers.js.
Remember that we have already created our token in an earlier post. Now we need to create a way for that to be shared amongst users who complete our survey.
First, in the scripts
folder, create two new files:
touch deploy-token.js deploy-owner.js
Then update them with the following code:
deploy-owner.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const Owner = await hre.ethers.getContractFactory("Owner");
const owner = await Owner.deploy();
await owner.deployed();
console.log("Owner deployed to:", owner.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
deploy-token.js
const hre = require("hardhat");
async function main() {
let ownerAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const CryptocracyToken = await hre.ethers.getContractFactory("CryptocracyToken");
const cryptocracyToken = await CryptocracyToken.deploy(ownerAddress, "CryptocracyToken", "CTK");
await cryptocracyToken.deployed();
console.log("CTK deployed to:", cryptocracyToken.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Then, navigate to the components
folder and create a new file, which will allow us to check our Metamask wallet balance and withdraw tokens.
touch token-utility.js
Update this file with the following code:
import React, {useState} from "react";
import {ethers} from "ethers";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import WalletBalanceDisplay from "./wallet-balance-display";
const TokenUtility = (props) => {
const tokenAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
const tokenOwnerAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const [withdrawalAmount, setWithdrawalAmount] = useState();
const [showWalletBalance, setShowWalletBalance] = useState(false);
const [newWalletBalance, updateNewWalletBalance] = useState();
const getWalletBalance = async () => {
if (typeof window.ethereum !== "undefined") {
const [account] = await window.ethereum.request({method: "eth_requestAccounts"});
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(tokenAddress, props.tokenContract.abi, provider);
const walletBalance = await contract.balanceOf(account);
updateNewWalletBalance(walletBalance.toString());
setShowWalletBalance(true);
}
};
const withdrawToken = async () => {
if (typeof window.ethereum !== "undefined") {
const account = await window.ethereum.request({method: "eth_requestAccounts"});
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const ownerContract = new ethers.Contract(tokenOwnerAddress, props.ownerContract.abi, signer);
let approveAllowance = await ownerContract.approveTokenSpend(tokenAddress, account[0], withdrawalAmount);
await approveAllowance.wait();
let withdraw = await ownerContract.withdrawToken(tokenAddress, account[0], withdrawalAmount);
await withdraw.wait();
}
};
return (
<div>
<Card>
<Card.Body>
<Card.Subtitle>Withdraw to Your Wallet
</Card.Subtitle><br/>
<div className="d-grid gap-2">
<input
onChange={e => setWithdrawalAmount(e.target.value)}
placeholder="Enter Amount"/>
<Button onClick={withdrawToken}>Withdraw</Button>
<Button onClick={getWalletBalance} variant="warning">Current
wallet balance</Button>
{showWalletBalance ? <WalletBalanceDisplay
balance={newWalletBalance}/> : null}
</div>
</Card.Body>
</Card>
</div>
);
};
export default TokenUtility;
And also create a file called wallet-balance-display.js
touch wallet-balance-display.js
and update it with the following code:
import Alert from "react-bootstrap/Alert"
const WalletBalanceDisplay = ({ balance }) => {
return (
<div>
<Alert variant="info"> Wallet balance: {balance}</Alert>
</div>
)
}
export default WalletBalanceDisplay
We also need to create a withdrawal container. Navigate to the components folder and type:
touch withdrawal-container.js
Update it with the following code:
import {Col, Container, Row} from "react-bootstrap";
import TokenUtility from "./token-utlity";
import CryptocracyToken from '../artifacts/contracts/CryptocracyToken.sol/CryptocracyToken.json'
import Owner from '../artifacts/contracts/Owner.sol/Owner.json'
export default function WithdrawalContainer() {
const Token = CryptocracyToken;
const TokenHolder = Owner;
return (
<>
<div className="withdrawal-container">
<Container>
<Row className="justify-content-md-center">
<Col>
<TokenUtility tokenContract={Token} ownerContract={TokenHolder}/>
</Col>
</Row>
</Container>
</div>
</>
);
}
You might see some errors at the top of the file because we haven't yet compiled our smart contracts yet. To do this, navigate to the source folder and type:
npm install @openzeppelin/contracts
which will install the Open Zeppelin library. Then type:
npx hardhat compile
This will compile the smart contracts and create a new folder in your src
folder called artifacts
. Your errors should now disappear.
Next, we need to get our mock Ethereum blockchain running. In another terminal window/tab:
npx hardhat node
You must leave this window open and not use it for any further commands. This is now continuously running. In another terminal window/tab and from the project root (not src), type:
npx hardhat run scripts/deploy-owner.js --network localhost
You should see this in response:
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Owner deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
then type:
npx hardhat run scripts/deploy-token.js --network localhost
to which you should see the following in response:
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
CTK deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Run the app:
npm start
Now, you need to import an account to Metamask, and the best choice here is the second account that appears in the Hardhat node. You shouldn't choose the first account because that is the account that is responsible for deploying the two smart contracts in our application. Find out how to import an account here:
https://metamask.zendesk.com/hc/en-us/articles/360015489331-How-to-import-an-Account
You will also need to import our custom CTK token to Metamask, which can be done by following the guide here:
Once complete, your Metamask should look something like this:
Now, when you click the "Current wallet balance" button you should see this:
And if you enter "50" in the input box and click the "Withdraw" button. You will see a FIRST Metamask confirmation box. Once you click "Confirm" you will see another confirmation box appear (which looks almost identical) - this is INTENDED because of our Owner/Approval setup in our smart contracts:
If you then click the "Current wallet balance" button again you will see that our balance is updated:
If you check this amount in Metamask, you will also see our balance has updated (but it will be in long decimal format):
Summary
In this series of blogs I have given a brief example of how to create a Web3 application using the Ethereum blockchain. The full application I developed was a lot bigger and more detailed. But hopefully this is at least an introduction of how to get started if you are interested becoming a Web3 developer.
Top comments (0)