Web3 Technology Stack丨Teach you how to build a full-stack dapp using EVM

Web3 Technology Stack丨Teach you how to build a full-stack dapp using EVM

Building a full-stack dApp using React, Ethers.js, Solidity, and Hardhat

In this tutorial, you will learn a web3 technology stack that will allow you to leverage the Ethereum Virtual Machine (EVM) to build full-stack applications on dozens of blockchain networks including Ethereum, Polygon, Avalanche, Celo, and many others.

The code for this project is here. The video course for this tutorial is here‌. Also check out Defining the web3 stack‌

I recently joined Edge & Node as a Developer Relations Engineer and have been diving deep into smart contract development on Ethereum. I have identified what I believe is the best stack for building a full-stack dApp using Solidity:

▶︎ Client-side framework - React

▶︎ Ethereum development environment — Hardhat‌

▶︎ Ethereum network client library - Ethers.js‌

▶︎ API layer - The Graph Protocol

The problem I had when learning this though was that while there was pretty good documentation for each of these things, there wasn't really anything on how to put all of these things together and understand how they all worked with each other. There are some really good templates out there like scaffold-eth (which also includes Ethers, Hardhat, and The Graph) but it can be too much for someone just getting started.

I wanted an end-to-end guide that showed me how to build a full-stack Ethereum application using the latest resources, libraries, and tools.

Things I'm interested in are:

  1. How to create, deploy and test Ethereum smart contracts to local, test and mainnet

  2. How to switch between local, test, and production environments/networks

  3. How to connect to and interact with contracts using various front-end environments such as React, Vue, Svelte, or Angular

After spending some time figuring all of this out and starting to use a stack that I’m pretty happy with, I thought it would be nice to write down how to build and test a full stack Ethereum application using this stack, not only for other people out there who might be interested in this stack, but also for my own future reference. This article is that reference.


Each part


Let’s review the main parts we’ll be using and how they fit into the stack.

1. Ethereum development environment

When building smart contracts, you will need a way to deploy contracts, run tests, and debug Solidity code without having to deal with a live environment.

You also need a way to compile your Solidity code into code that can be run in a client application - in our case, a React application. We'll look at how this works in detail later.

Hardhat is an Ethereum development environment and framework designed for full-stack development, and is the framework I will use for this tutorial.

Other similar tools in the ecosystem are Ganache, Truffle, and Foundry.

2. Ethereum network client library

In our React application, we need a way to interact with our deployed smart contract. We will need a way to read data as well as send new transactions.

ethers.js aims to be a complete and compact library for interacting with the Ethereum blockchain and its ecosystem from client-side JavaScript applications like React, Vue, Angular, or Svelte. This is the library we will use.

Another popular option in the ecosystem is web3.js

3. Metamask

Metamask helps handle account management and connects the current user to the blockchain. MetaMask enables users to manage their accounts and keys in several different ways while isolating them from the site context.

Once a user has connected their MetaMask wallet, you as a developer can interact with the globally available Ethereum API (window.ethereum) which recognizes users of web3 compatible browsers as MetaMask users, and whenever you request a signature for a transaction, MetaMask will prompt the user in the most understandable way possible.

4. React

React is a front-end JavaScript library for building web applications, user interfaces, and UI components. It is maintained by Facebook and many individual developers and companies.

React and its vast ecosystem of meta-frameworks (like Next.js, Gatsby, Redwood, Blitz.js, etc.) support all types of deployment targets, including traditional SPAs, static site generators, server-side rendering, and combinations of all three. React seems to continue to dominate the front-end space, and I think it will continue to do so for at least the near future.

5. The Graph

For most applications built on blockchains like Ethereum, reading data directly from the chain is difficult and time-consuming, so you used to see people and companies build their own centralized index servers and serve API requests from those servers. This requires a lot of engineering and hardware resources and undermines the security properties required for decentralization.

The Graph is an indexing protocol for querying blockchain data that makes it possible to create fully decentralized applications and solves this problem, providing a rich GraphQL query layer that applications can use. In this guide, we will not build a subgraph for our application, but will do so in a later tutorial.

To learn how to build a blockchain API with The Graph, check out Building a GraphQL API on Ethereum‌.


What we will build


In this tutorial, we will build, deploy, and connect several basic smart contracts:

  1. Contracts that create and update messages on the Ethereum blockchain

  2. A contract for minting tokens, which then allows the owner of the contract to send tokens to other people and read token balances, and allows the owner of the new tokens to send them to other people as well.

We will also build a React frontend that allows users to:

  1. Read the greeting from the contract deployed to the blockchain

  2. Update Greetings

  3. Send newly minted tokens from their address to another address

  4. Once someone receives tokens, allow them to also send their tokens to others

  5. Read token balances from contracts deployed to the blockchain

Prerequisites

  • Node.js installed on your local machine

  • MetaMask, a Chrome extension installed in your browser

For this guide, you do not need to own any Ethereum as we will be using fake/test Ether on a test network throughout the tutorial.


start


First, we'll create a new React application:

npx create-react-app react-dapp

Next, move into the new directory and install ethers.js and hardhat using NPM or Yarn:

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

Install and configure the Ethereum development environment

Next, initialize a new Ethereum development environment using Hardhat:

npx hardhat

? What do you want to do? Create a sample project
? Hardhat project root:

You should now see the following section created for you in your root directory:

  • hardhat.config.js - Your entire Hardhat setup (that is, your configuration, plugins, and custom tasks) is contained in this file.

  • scrips - A folder containing a script called sample-script.js which will deploy your smart contract when executed

  • test - Folder containing example test scripts contracts - Folder containing example Solidity smart contracts

Due to a MetaMask configuration issue, we need to update the chain ID on the HardHat configuration to 1337. We also need to update the artifact location of the compiled contract to be in the src directory of the React application.

To make these updates, open hardhat.config.js and update module.exports to look like this:

module.exports = {
solidity: "0.8.4",
paths: {
artifacts: './src/artifacts',
},
networks: {
hardhat: {
chainId: 1337
}
}
};


Our Smart Contract


Next, let's look at the example contract provided to us in contracts/Greeter.sol:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";


contract Greeter {
string greeting;

constructor(string memory _greeting) {
console.log("Deploying a Greeter with greeting:", _greeting);
greeting = _greeting;
}

function greet() public view returns (string memory) {
return greeting;
}

function setGreeting(string memory _greeting) public {
console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
greeting = _greeting;
}
}

This is a very basic smart contract. When deployed, it sets a Greeting variable and exposes a function (greet) that can be called to return a greeting.

It also exposes a function (setGreeting) that allows the user to update the greeting. When deployed to the Ethereum blockchain, these methods will be available for user interaction.


Read and write to the Ethereum blockchain


There are two ways to interact with a smart contract, reading or writing/transactional. In our contract, greet can be thought of as reading, while setGreeting can be thought of as writing/transactional.

When writing or initializing a transaction, you must pay for the transaction to be written to the blockchain. To do this, you need to pay Gas, which is the fee or price required to successfully conduct transactions and execute contracts on the Ethereum blockchain.

As long as you are only reading data from the blockchain and not changing or updating anything, you do not need to perform a transaction and there is no gas or cost for doing so. The functions you call are then only executed by the nodes you are connected to, so you do not pay anything and reads are free.

In our React application, the way we interact with the smart contract is by using a combination of the ethers.js library, the contract address, and the ABI that will be created from the contract by hardhat.

What is ABI? ABI stands for Application Binary Interface. You can think of it as an interface between your client application and the Ethereum blockchain where the smart contracts you will interact with are deployed.

ABIs are often compiled from Solidity smart contracts by development frameworks such as HardHat. You can also often find the ABI of a smart contract on Etherscan.


Compiling ABI


Now that we have understood the basics of smart contracts and know what an ABI is, let’s compile an ABI for our project.

To do this, go to the command line and run the following command:

npx hardhat compile

You should now see a new folder called artifacts in your src directory.

The artifacts/contracts/Greeter.json file contains the ABI as one of its properties. When we need to use the ABI, we can import it from our JavaScript file:

import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

We can then reference the ABI like this:

console.log("Greeter ABI: ", Greeter.abi)

Note that Ethers.js also enables human-readable ABI, but we won’t cover that in this tutorial.


Deploy and use a local network/blockchain


Next, let’s deploy our smart contract to the local blockchain so we can test it.

To deploy to your local network, you first need to start a local test node. To do this, open the CLI and run the following command:

npx hardhat node

When we run this command you should see a list of addresses and private keys.

These are 20 test accounts and addresses created for us that we can use to deploy and test our smart contracts. Each account also has 10,000 test ether loaded into it. Later, we will learn how to import the test account into MetaMask so that we can use it.

Next, we need to deploy the contract to the test network. First, update the name of scripts/sample-script.js to scripts/deploy.js.

Now we can run the deployment script and provide a flag to the CLI that we want to deploy to our local network:

npx hardhat run scripts/deploy.js --network localhost

After executing this script, the smart contract should be deployed to the local test network, and we should then be able to start interacting with it.

When the contract was deployed, it used the first account we created when we started the local network.

If you look at the output of the CLI, you should be able to see something like this:

Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

This address is what we will use in our client application to talk to the smart contract. Keep this address available as we will need to use it when connecting to it from our client application.

To send transactions to the smart contract, we need to connect our MetaMask wallet using one of the accounts we created when running the npx hardhat node. In the list of contracts logged out of the CLI, you should see the Account number and Private Key :

➜ react-dapp git:(main) npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

...

We can import this account into MetaMask in order to start using some of the Eth test coins available there.

To do this, first open MetaMask and enable the test network:

Next, update the network to Localhost 8545:

Next, in MetaMask, click “Import Accounts” from the accounts menu:

Copy and paste one of the Private Keys logged out via the CLI and click Import. Once the account is imported you should see Eth in your account:

Now that we have a smart contract deployed and an account to use, we can start interacting with it from our React application.


Connecting the React Client


In this tutorial we're not going to worry about building a nice UI with CSS and all that, we're going to focus 100% on the core functionality to get you up and running. You can take it from there and make it look nice if you want.

That being said, let's review the two goals we want to achieve from a React application:

  1. Get the current greeting value from the smart contract

  2. Allows the user to update the greeting value

Knowing this, how do we do this? To achieve this, we need to do the following:

  1. Create an input field and some local state to manage the input value (update greeting)

  2. Allows applications to connect to the user's MetaMask account to sign transactions

  3. Create functions for reading and writing smart contracts

To do this, open src/App.js and update it with the following code, setting the value of greeterAddress to the address of your smart contract.

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

// Update with the contract address logged out to the CLI when it was deployed
const greeterAddress = "your-contract-address"

function App() {
// store greeting in local state
const [greeting, setGreetingValue] = useState()

// request access to the user's MetaMask account
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}

// call the smart contract, read the current greeting value
async function fetchGreeting() {
if (typeof window.ethereum !== 'undefined') {
const provider = new ethers.providers.Web3Provider(window.ethereum)
const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
try {
const data = await contract.greet()
console.log('data: ', data)
} catch (err) {
console.log("Error: ", err)
}
}
}

// call the smart contract, send an update
async function setGreeting() {
if (!greeting) return
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner()
const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
const transaction = await contract.setGreeting(greeting)
await transaction.wait()
fetchGreeting()
}
}

return (
<div className="App">
<header className="App-header">
<button onClick={fetchGreeting}>Fetch Greeting</button>
<button onClick={setGreeting}>Set Greeting</button>
<input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
</header>
</div>
);
}

export default App;

To test it, start the React server:

npm start

When the application loads, you should be able to get the current greeting and log it out to the console. You should also be able to update the greeting by signing the contract with your MetaMask wallet and using Ether test coins.


Deploy and use a live test network


There are several Ethereum test networks like Ropsten, Rinkeby or Kovan that we can also deploy to in order to get a publicly accessible version of the contract without having to deploy it to the mainnet. In this tutorial, we will deploy to the Ropsten test network.

First, start by updating your MetaMask wallet to connect to the Ropsten network.

Next, send yourself some test Ether to use throughout the rest of this tutorial by visiting this test faucet.

We can access Ropsten (or any other test network) by signing up with a service like Infura or Alchemy (I use Infura in this tutorial).

Once you create an application in Infura or Alchemy, you will get an endpoint that looks like this:

https://ropsten.infura.io/v3/your-project-id

Be sure to set ALLOWLIST ETHEREUM ADDRESSES in your Infura or Alchemy application configuration to include the wallet address of the account you will be using for deployment.

To deploy to the test network, we need to update our hardhat configuration with some additional network information. One of the things we need to set up is the private key of the wallet we will be deploying from.

To get your private key, you can export it from MetaMask.

I would recommend not hard coding this value in your application, but instead setting it as something like an environment variable.

Next, add a network property with the following configuration:

module.exports = {
defaultNetwork: "hardhat",
paths: {
artifacts: './src/artifacts',
},
networks: {
hardhat: {},
ropsten: {
url: "https://ropsten.infura.io/v3/your-project-id",
accounts: [`0x${your-private-key}`]
}
},
solidity: "0.8.4",
};

To deploy, run the following script:

npx hardhat run scripts/deploy.js --network ropsten

Once the contract is deployed, you should be able to start interacting with it. You should now be able to view the live contract on the Etherscan Ropsten testnet explorer


Minting Tokens


One of the most common use cases for smart contracts is creating tokens, let’s look at how we can do that. We’ll go a little faster now that we understand a little more about how all of this works.

Create a new file called Token.sol in the main contracts directory.

Next, update Token.sol with the following smart contract:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Token {
string public name = "Nader Dabit Token";
string public symbol = "NDT";
uint public totalSupply = 1000000;
mapping(address => uint) balances;

constructor() {
balances[msg.sender] = totalSupply;
}

function transfer(address to, uint amount) external {
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
}

function balanceOf(address account) external view returns (uint) {
return balances[account];
}
}

Please note that this token contract is for demonstration purposes only and is not ERC20 compliant. We will cover ERC20 tokens here

This contract will create a new token called “Nader Dabit Token” and set the supply to 1,000,000.

Next, compile the contract:

npx hardhat compile

Now, update the deploy script in scripts/deploy.js to include this new token contract:

const hre = require("hardhat");

async function main() {
const [deployer] = await hre.ethers.getSigners();

console.log(
"Deploying contracts with the account:",
deployer.address
);

const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, World!");

const Token = await hre.ethers.getContractFactory("Token");
const token = await Token.deploy();

await greeter.deployed();
await token.deployed();

console.log("Greeter deployed to:", greeter.address);
console.log("Token deployed to:", token.address);
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});

We can now deploy this new contract locally or to the Ropsten network:

npx hardhat run scripts/deploy.js --network localhost

Once the contract is deployed, you can start sending these tokens to other addresses.

To do this, let's update the client code we need to make it work:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'
import Token from './artifacts/contracts/Token.sol/Token.json'

const greeterAddress = "your-contract-address"
const tokenAddress = "your-contract-address"

function App() {
const [greeting, setGreetingValue] = useState()
const [userAccount, setUserAccount] = useState()
const [amount, setAmount] = useState()

async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}

async function fetchGreeting() {
if (typeof window.ethereum !== 'undefined') {
const provider = new ethers.providers.Web3Provider(window.ethereum)
console.log({ provider })
const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
try {
const data = await contract.greet()
console.log('data: ', data)
} catch (err) {
console.log("Error: ", err)
}
}
}

async function getBalance() {
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, Token.abi, provider)
const balance = await contract.balanceOf(account);
console.log("Balance: ", balance.toString());
}
}

async function setGreeting() {
if (!greeting) return
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
console.log({ provider })
const signer = provider.getSigner()
const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
const transaction = await contract.setGreeting(greeting)
await transaction.wait()
fetchGreeting()
}
}

async function sendCoins() {
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(tokenAddress, Token.abi, signer);
const transation = await contract.transfer(userAccount, amount);
await transation.wait();
console.log(`${amount} Coins successfully sent to ${userAccount}`);
}
}

return (
<div className="App">
<header className="App-header">
<button onClick={fetchGreeting}>Fetch Greeting</button>
<button onClick={setGreeting}>Set Greeting</button>
<input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />

<br />
<button onClick={getBalance}>Get Balance</button>
<button onClick={sendCoins}>Send Coins</button>
<input onChange={e => setUserAccount(e.target.value)} placeholder="Account ID" />
<input onChange={e => setAmount(e.target.value)} placeholder="Amount" />
</header>
</div>
);
}

export default App;

Next, run the application:

npm start

We should be able to click "Get Balance" and see 1,000,000 coins in our account logged out to the console.

You should also be able to view them in MetaMask by clicking Import Tokens:

Next click on Custom Token and enter the token contract address, then add your custom token. (If asked for token decimals, select 0) Now the token should be available in your wallet:

Next, let's try sending these coins to another address.

To do this, copy the address of another account and then send them to that address using the updated React UI. When you check the token amount, it should be equal to the original amount minus the amount you sent to the address.


ERC20 Tokens


The ERC20 token standard defines a set of rules that apply to all ERC20 tokens, allowing them to easily interact with each other. ERC20 makes it easy for people to mint their own tokens that will be interoperable with others on the Ethereum blockchain.

Let’s see how we can build our own token using the ERC20 standard.

First, install the OpenZepplin smart contract library, into which we will import the base ERC20 token:

npm install @openzeppelin/contracts

Next, we will create our token by extending (or inheriting from) the ERC20 contract:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NDToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 100000 * (10 ** 18));
}
}

The constructor allows you to set the token name and symbol, and the _mint function allows you to mint tokens and set the amount.

By default, ERC20 sets the number of decimal places to 18, so in our _mint function, we multiply 100,000 by 10 to the 18th power to mint a total of 100,000 tokens, each with 18 decimal places (similar to how 1 Eth is made up of 10 to 18 wei.)

To deploy, we need to pass in the constructor values ​​(name and symbol), so we can do the following in our deployment script:

const NDToken = await hre.ethers.getContractFactory("NDToken");
const ndToken = await NDToken.deploy("Nader Dabit Token", "NDT");

By extending the original ERC20 token, your token will inherit all of the following features and functionalities:

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)

Once deployed, you can use any of these functions to interact with your new smart contract. For another example of an ERC20 token, check out [Solidity by example)( https://solidity-by-example.org/app/erc20/)


in conclusion


Ok, we covered a lot here, but for me this was the basics/core to get started with this stack, and it's something I want to have, not only as someone who is learning all this stuff, but also in the future if I need to reference anything I might need in the future. I hope you learned a lot.

If you want to support multiple wallets in addition to MetaMask, check out Web3Modal, which makes it easy to implement support for multiple providers in your app with a fairly simple and customizable configuration.

In my future tutorials and guides, I will dive deeper into more complex smart contract development and how to deploy them as subgraphs to expose GraphQL APIs on top of them and implement features like pagination and full-text search.

I will also cover how to use technologies like IPFS and Web3 databases to store data in a decentralized manner.


<<:  The country's first digital RMB roadside parking scene was launched in Shenzhen

>>:  Grayscale Bitcoin Trust's negative premium hits 26.5%, a new low. Will Bitcoin ETF become a new battlefield?

Recommend

What does a passionate and philandering man look like?

What does a passionate and philandering man look ...

How to read personality through online face analysis

As the saying goes, "Appearance is determine...

A woman's fate as seen from her physical features

A woman's fate as seen from her physical feat...

Palmistry of a woman born as a rich second generation

There are indeed many poor people in the world to...

The miners' mood has improved. Is the mine disaster coming to an end?

What do you think when you see the picture above?...

Palmistry to predict life expectancy

You can tell a person's life span by looking ...

A kind-hearted person with good fortune

You can tell whether a person is kind by looking ...

People who don't know how to refuse others in life

In life, more and more people don’t know how to r...

How to view the creative line? Detailed explanation of the creative line

What is the creation line? What kind of influence...

How to read women's eyebrows and facial features

In physiognomy, one can tell a person’s fate and ...

Is it true that a mole in the middle of a woman's chin means good luck in love?

How to interpret a mole on a woman’s chin? In our...

Your facial complexion shows your fortune

Your facial complexion shows your fortune Facial ...