DEV Community

Cover image for A JavaScript journey: Web3 app
Jim
Jim

Posted on

A JavaScript journey: Web3 app

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

We can also set up our Ethereum environment here by entering:

npx hardhat
Enter fullscreen mode Exit fullscreen mode

then choose:

Create a basic sample project
Enter fullscreen mode Exit fullscreen mode

This will create a file called hardhat.config.js and create two new folders in your root directory:

scripts
contracts
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Next we need to install two more libraries which are called by require in the file above:

npm install @nomiclabs/hardhat-waffle dotenv
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Front-end

Create client directories

Navigate to the src folder and create three directories:

mkdir 
components 
pages 
stylesheets
Enter fullscreen mode Exit fullscreen mode

Create homepage file

Navigate to the pages folder and type the following to create your homepage file.

touch home-page.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

then navigate to the stylesheets folder and type the following into the terminal

touch home-page-hero.css navbar.css global-button.css
Enter fullscreen mode Exit fullscreen mode

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%;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;


}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Also, go to index.js and remove the following line:

import reportWebVitals from './reportWebVitals';
Enter fullscreen mode Exit fullscreen mode

Then, in the terminal, run

npm start
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Create the dashboard

Navigate to the pages folder and create a new file:

touch dashboard-page.js
Enter fullscreen mode Exit fullscreen mode

update this file with the following code:

import Navbar from "./components/navbar";

export default function DashboardPage(){
    return (
        <>
            <Navbar/>
            <DashboardPageHero/>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

then navigate to the components folder and create a new file:

touch dashboard-page-hero.js
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

Also, add the following line to App.js:

<Route exact path='/surveys' element={<DashboardPage/>} replace/>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create a new folder in the client folder:

mkdir surveys
Enter fullscreen mode Exit fullscreen mode

Navigate to this folder and create two files:

touch survey-control.js survey-questions.js
Enter fullscreen mode Exit fullscreen mode

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

The survey should look something like this:

Image description

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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);
    });

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

And also create a file called wallet-balance-display.js

touch wallet-balance-display.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We also need to create a withdrawal container. Navigate to the components folder and type:

touch withdrawal-container.js
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

which will install the Open Zeppelin library. Then type:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should see this in response:

Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Owner deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

then type:

npx hardhat run scripts/deploy-token.js --network localhost
Enter fullscreen mode Exit fullscreen mode

to which you should see the following in response:

Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
CTK deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Enter fullscreen mode Exit fullscreen mode

Run the app:

npm start
Enter fullscreen mode Exit fullscreen mode

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:

https://metamask.zendesk.com/hc/en-us/articles/360015489031-How-to-add-unlisted-tokens-custom-tokens-in-MetaMask#h_01FWH492CHY60HWPC28RW0872H

Once complete, your Metamask should look something like this:

Image description

Now, when you click the "Current wallet balance" button you should see this:

Image description

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:

Image description

If you then click the "Current wallet balance" button again you will see that our balance is updated:

Image description

If you check this amount in Metamask, you will also see our balance has updated (but it will be in long decimal format):

Image description

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)