Zero to One Full-Stack DApp Ethereum Development based on Foundry, NextJS, Typescript - Latest Version

Zero to One Full-Stack DApp Ethereum Development based on Foundry, NextJS, Typescript - Latest Version

Why

Recently, I studied a lot of knowledge about the Ethereum ecosystem.
I can’t wait to create something by myself.
But I searched for a long time without finding a basic tutorial for me to explain how the front-end and chain-end interact.
Some tutorials are old. Other tutorials don’t match my tech stack.
I know scaffold-eth can help most fresh developers begin Ethereum development. However, scaffold-eth has too much magic for me. It hides some basic things. And I want to know the full details.
So I reckon I could write some articles from web2 developer to web3 beginner DApp developer in 2022. It’s a cool thing.

Learning DApp architecture

First thing, we need to study about DApp architecture.

When we have read through this The Architecture of a Web 3.0 application and the knowledge we have so far.

A fully DApp architecture looks like this:

Fully DApp Architecture

For beginner, I draw the simply DApp architecture

Simply DApp architecture

Look at the simply architecture, there are two important questions:

  1. How do browsers and websites interact with each other?
  2. How do websites and blockchains interact with each other?

Simple Answers:

  1. Browser’s wallet extension or Wallet App
  2. JSON RPC

Then, we will explore these two questions carefully when building a minimal DApp.

What are you best to know

  • Metamask
  • React
  • Node
  • Solidity

Frameworks and technologies

In this article, we will use below frameworks and technologies.

Front-end

  1. NextJS for our front-end development and deployment
  2. Typescript for type safety and cleaner code
  3. Tailwind CSS (option) I love Tailwind CSS.
  4. Flowbite (option) It is an open-source component library from Tailwind CSS

Chain-end

  1. Foundry It is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
  2. ganache Personal blockchain for Ethereum development

Why I chose Foundry instead of Hardhat?

  1. Simple, foundry doesn’t care about any javascript things.
  2. Pure Solidity, we can write unit tests in Solidity.

Why I used ganache and not Anvil?

  1. When I was using anvil as a local chain I encountered the transfer jamming bug.
  2. I like simple things, so ganache has a GUI. It’s even easier for a beginner.

What is a minimal DApp?

  1. We will use the default smart contract when we initiate the chain end project.
  2. We will build simple front end and blockchain interaction logic.

Like this:

Let’s do it.

Create Our Project From Scratch

Setting up the workspace

I wish I could separate the front-end app and the chain-end app.
Keep simple, keep pure.

Create the project folder, which we call the DApp-Demo

1
2
mkdir DApp-Demo
cd DApp-Demo

Add ./DApp-Demo/.editorconfig file

1
2
3
4
5
6
7
8
9
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Create chain end application with Foundry

Install Foundry

1
curl -L https://foundry.paradigm.xyz | bash

We will use forge to start a new chain-end project.

1
2
# on ./DApp-Demo
forge init chain_end

For now, let’s check what the project layout looks like:

1
2
3
4
5
6
7
8
9
10
tree -L 2
.
└── chain_end
├── foundry.toml
├── lib
├── script
├── src
└── test

5 directories, 1 file

We will get a sample counter smart contract application.
In our case, we don’t need to do any modifications.

Open ./DApp-Demo/chain_end/src/Counter.sol file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}

Our final task is that our front-end application could call these two functions on a chain.

Tips

If we use VSCode to open this workspace, we will see solidity code has some warnings. Looks like:

We could add VSCode setting to solve this issue.
create ./DApp-Demo/.vscode/settings.json

1
2
3
{
"solidity.packageDefaultDependenciesDirectory": "chain_end/lib"
}

Now the VSCode looks good.

Also, we could try forge test command

1
2
3
4
5
6
7
8
9
10
11
# on ./DApp-Demo/chain_end/
forge test
[⠢] Compiling...
[⠢] Compiling 10 files with 0.8.17
[⠰] Solc 0.8.17 finished in 992.00ms
Compiler run successful

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 28312)
[PASS] testSetNumber(uint256) (runs: 256, μ: 27531, ~: 28387)
Test result: ok. 2 passed; 0 failed; finished in 6.19ms

For now so far so good.

Create front end application

  1. Initial our front end project with nextjs, typescript and tailwindcss.
1
2
# on ./DApp-Demo
yarn create next-app -e with-tailwindcss front_end

For now, let’s check what the project layout looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
tree -L 2
.
├── chain_end
│   ├── cache
│   ├── foundry.toml
│   ├── lib
│   ├── out
│   ├── script
│   ├── src
│   └── test
└── front_end
├── README.md
├── next-env.d.ts
├── next.config.js
├── node_modules
├── package.json
├── pages
├── postcss.config.js
├── public
├── styles
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock

12 directories, 9 files
  1. Add front_end/.prettierrc file
1
2
3
4
5
6
{
"semi": false,
"trailingComma": "all",
"singleQuote": true
}

  1. Install Flowbite React UI component library

Official Document

Install the flowbite package

1
2
3
cd front_end
# on ./DApp-Demo/front_end
yarn add flowbite flowbite-react

Update tailwind.config.js

1
2
3
4
5
6
7
8
9
10
11
12
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./node_modules/flowbite-react/**/*.js',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [require('flowbite/plugin')],
}

Here, we completed the front-end application set up.

Initial Git

We want to use git to manage our code.
However, we used forge and yarn to create projects. These two projects chain_end and front_end already have .git folders.
So first we delete those .git folders, Second, we initial the new git history.

1
2
3
4
5
6
# on ./DApp-Demo
rm -rf chain_end/.git
rm -rf front_end/.git
git init
git add .
git commit -m "init project"

Setting up the local chain for development

Usually, We have three choose to run a chain on localhost.

  1. HardHat - HardHat Network
  2. Truffle - ganahce
  3. Foundry - anvil

Generally, we should have chosen anvil. It’s a part of Foundry.
Unfortunately, there were a lot of transitions pending when I used it. From my point of view anvil just is the 0.1.0 version. It will be need more time to develop.
And I want to keep pure in chain end’s project(Not more javascript or npm install).
Finally, we used the ganahce to run chain on localhost.

The ganahce is very easy to install and setting up. It’s a GUI application.

How to use the ganchce

First, Install ganahce.

Open ganahce official website Download it and install it.

Create an new workspace

  1. Choose ‘ETHEREUM’ and click the button.

image.png

  1. Rename the workspace name

We can modify it to any name we like.
Here, I used my ethereum

image.png

  1. Update the server’s port and network id

The PORT NUMBER is 8545.
The NETWORK ID is 1337.
There are import things. They will match the MetaMask Wallet’s setting.

image.png

  1. Open Lock Accounts

We don’t like the account always to change.

image.png

  1. Click SAVE WORKSPACE button

image.png

  1. Done

image.png

Setting up the MetaMask on localhost’s chain

  1. Install MetaMask and create one wallet.

https://metamask.io/

  1. Open show test networks

Settings > Advanced

image.png

Now we can see all test networks

image.png

  1. Add our localhost chain wallet to MetaMask

Open ganache and copy the private key

image.png

image.png

Import private key to MetaMask wallet

image.png

image.png

Now we switch the network to Localhost 8545 and Account 2

image.png

image.png

We can see

image.png

We do it again to add one more account

Try one transition on localhost chain

Finally we already set up the localhost chain environment.

Let’s try one transition

Copy one wallet’s address

image.png

Switch other wallet’s account and send eth to the copy address

image.png
image.png
image.png

Wait a few seconds
image.png

Check ganache

image.png
image.png
image.png
image.png

Awesome, we have our own ethereum!

Deploying the smart contract to a localhost chain

We remember the Foundry includes forge, cast, and anvil.
We will use forge to deploy our smart contracts. Then we will try to call our smart contract on cast

Deploying

The first important thing is that we need to keep running the local chain on ganache.

We will run like below command to deploy a smart contract

forge create --rpc-url <your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract

This your_rpc_url can be found here.
image.png

This your_private_key is your deploy wallet private key. It can be found here.

image.png
image.png

We fill our value to the command, my command likes:

1
2
3
4
5
# on ./DApp-Demo
cd chain_end
forge create --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c src/Counter.sol:Counter --legacy
# or we can install https://github.com/stedolan/jq. Then we can format the output. It is option
forge create --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c src/Counter.sol:Counter --json --legacy | jq

Because ganchce runs an old version chain so we need to add --legacy.

We will get like this:

1
2
3
4
5
6
forge create --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c src/Counter.sol:Counter --json --legacy | jq
{
"deployedTo": "0x77D4DD041918061A42759fe1ECDe60fC67d28317",
"deployer": "0xDd7Eaf8eFF926aD81f5d4bBd68568CD1Fe4B1869",
"transactionHash": "0x41d17867b64fdca61bd6182ba6548b78b867d8d81530bed82d988646b3259d39"
}

The deployedTo value 0x77D4DD041918061A42759fe1ECDe60fC67d28317 is the smart contract address.

Now, our smart contract already has been deployed successfully.

So easy! Right?

One more thing, because we use ganacha, we can check the smart contract deploy information on GUI too.

image.png

Uses cast to call the smart contract on the localhost chain

In fact, everyone use the ethereum’s JSON_RPC API to call smart contract.
Today we will use eth_call and eth_sendTransaction.

The difference between a call and a transaction is the following:

  1. transactions are created by your client, signed, and broadcasted to the network. They will eventually alter the state of the blockchain, for example, by manipulating balances or values in smart contracts.

  2. calls are transactions executed locally on the user’s local machine which alone evaluates the result. These are read-only and fast. They can’t change the blockchain in any way because they are never sent to the network. Some examples “read-only/dry run/practice”.

Call

cast call - Perform a call on an account without publishing a transaction. More Detail

It is Ethereum JSON-RPC’s eth_call

Transaction

cast send - Sign and publish a transaction. More Detail

It is Ethereum JSON-RPC’s eth_sendTransaction

Use cast call

We will run like below command to get a smart contract’s a public variable
cast call your_smart_contract_address "public_variable_name()" --rpc-url http://127.0.0.1:8545

First, we need to find the smart contract address 0x77D4DD041918061A42759fe1ECDe60fC67d28317.

Second, our smart contract code is (chain_end/src/Counter.sol)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}

So public_variable_name is number.

We could get this command

1
2
3
cast call 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "number()" --rpc-url http://127.0.0.1:8545
Output:
0x0000000000000000000000000000000000000000000000000000000000000000

Use cast send

We will run like below command to call a smart contract’s a function

cast send your_smart_contract_address "function_name(function_args_type)(function_return_type)" --rpc-url http://127.0.0.1:8545 --private-key your_private_key --legacy --json | jq

Based on before content, we know the your_smart_contract_address and can find the your_private_key.

The increment function

1
2
3
function increment() public {
number++;
}

uses function_name(function_args_type)(function_return_type) method will convert to increment()

The setNumber function

1
2
3
function setNumber(uint256 newNumber) public {
number = newNumber;
}

uses function_name(function_args_type)(function_return_type) will convert to setNumber(uint256)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cast send 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "increment()" --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c --legacy --json | jq
Output:
{
"blockHash": "0x627958c966a6cae190d0dadd513cfdd5f53eaa048932394bd343cbb0fe3add00",
"blockNumber": "0x15",
"contractAddress": null,
"cumulativeGasUsed": "0xa478",
"from": "0xdd7eaf8eff926ad81f5d4bbd68568cd1fe4b1869",
"gasUsed": "0xa478",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0x77D4DD041918061A42759fe1ECDe60fC67d28317",
"transactionHash": "0x9ee54c94adf35f3c9d4c18525266ff20e2210cd410cd3fa7d8ac682a4456b021",
"transactionIndex": "0x0"
}

Next, we will repeat multiple times to call our smart contract function.

1
2
3
cast call 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "number()" --rpc-url http://127.0.0.1:8545
Output:
0x0000000000000000000000000000000000000000000000000000000000000001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cast send 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "increment()" --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c --legacy --json | jq
Output:
{
"blockHash": "0x6c3e9052c3e898083184c2409915a403dc73f17ee5ebed7d58b5a021ae38b8c2",
"blockNumber": "0x16",
"contractAddress": null,
"cumulativeGasUsed": "0x69e0",
"from": "0xdd7eaf8eff926ad81f5d4bbd68568cd1fe4b1869",
"gasUsed": "0x69e0",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0x77D4DD041918061A42759fe1ECDe60fC67d28317",
"transactionHash": "0xcb506d3895e30c2cbcfb2f49063e1d598994499d980fb4eb7821d5edcabca3fa",
"transactionIndex": "0x0"
}
1
2
3
cast call 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "number()" --rpc-url http://127.0.0.1:8545
Output:
0x0000000000000000000000000000000000000000000000000000000000000002
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cast send 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "setNumber(uint256)" 100 --rpc-url http://127.0.0.1:8545 --private-key 1430bfebd9c4cf85cd2c9ccb43b7b1f6e3aa3449cd98451ad23ac31f518c3f7c --legacy --json | jq
Output:
{
"blockHash": "0x5950ceaadc6177b0552396c208af2064d1b9272c796d8e4209252c8e048bbf83",
"blockNumber": "0x17",
"contractAddress": null,
"cumulativeGasUsed": "0x671a",
"from": "0xdd7eaf8eff926ad81f5d4bbd68568cd1fe4b1869",
"gasUsed": "0x671a",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0x77D4DD041918061A42759fe1ECDe60fC67d28317",
"transactionHash": "0x5acc3e10d0e7a92cd5117d77810f0ffe31c0ee8e3b831577d6d0c7733d36452c",
"transactionIndex": "0x0"
}
1
2
3
cast call 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "number()" --rpc-url http://127.0.0.1:8545
Output:
0x0000000000000000000000000000000000000000000000000000000000000064
1
2
3
cast call 0x77D4DD041918061A42759fe1ECDe60fC67d28317 "number()(uint256)" --rpc-url http://127.0.0.1:8545
Output:
100

So far so good.

Now we can deploy the smart contract on the local chain and call its function successfully.

Build front-end project

We will focus how to build a minimal DApp. So I won’t explain any front end code details.

In the beginning, we quickly build our home page’s layout on the front-end project.

We use below code to replace front_end/pages/index.tsx file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import type { NextPage } from "next";
import { Navbar, Footer, Button } from "flowbite-react";

const Home: NextPage = () => {
const handleConnectWallet = () => {};
return (
<div className="">
<Navbar fluid={true} rounded={true}>
<Navbar.Brand href="/">
<img
src="https://flowbite.com/docs/images/logo.svg"
className="mr-3 h-6 sm:h-9"
alt="Flowbite Logo"
/>
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
DApp Demo
</span>
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
<Button onClick={handleConnectWallet}>Connect Wallet</Button>
</Navbar.Collapse>
</Navbar>
<div className="min-w-full min-h-full">
<div className="container flex flex-col justify-center items-center space-y-5"></div>
</div>
<Footer container={true}>
<Footer.Copyright
href="#"
by="OhMyApps™"
year={new Date().getFullYear()}
/>
<Footer.LinkGroup>
<Footer.Link href="#">About</Footer.Link>
<Footer.Link href="#">Privacy Policy</Footer.Link>
<Footer.Link href="#">Licensing</Footer.Link>
<Footer.Link href="#">Contact</Footer.Link>
</Footer.LinkGroup>
</Footer>
</div>
);
};

export default Home;

image.png

How to use ethers.js

Let’s try to build the first feature with ethers.js

Install ethers.js

The ethers.js library is interacting with Ethereum. So we choose it.

Now, We install it.

1
2
3
// on ./DApp-Demo
cd front_end
yarn add ethers

Build a button that connects our wallet with ethers.js

We will use metamask to connect to our website. Looks like

2022-10-22 23.20.04.gif

We study the ethersjs document,

We changed our front_end/pages/index.tsx file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { useState } from 'react'
import type { NextPage } from 'next'
import { Navbar, Footer, Button } from 'flowbite-react'
import { ethers } from 'ethers'

declare let window: any

const Home: NextPage = () => {
const [address, setAddress] = useState<string>()
const [balance, setBalance] = useState<string>()
const handleConnectWallet = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum)
await provider.send('eth_requestAccounts', [])
const signer = provider.getSigner()
setAddress(await signer.getAddress())
setBalance(ethers.utils.formatEther(await signer.getBalance()))
}
return (
<div className="">
<Navbar fluid={true} rounded={true}>
<Navbar.Brand href="/">
<img
src="https://flowbite.com/docs/images/logo.svg"
className="mr-3 h-6 sm:h-9"
alt="Flowbite Logo"
/>
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
DApp Demo
</span>
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
{address ? (
<>
<div>{address}</div>
<div>{balance}</div>
</>
) : (
<Button onClick={handleConnectWallet}>Connect Wallet</Button>
)}
</Navbar.Collapse>
</Navbar>
<div className="min-w-full min-h-full">
<div className="container flex flex-col justify-center items-center space-y-5"></div>
</div>
<Footer container={true}>
<Footer.Copyright
href="#"
by="OhMyApps™"
year={new Date().getFullYear()}
/>
<Footer.LinkGroup>
<Footer.Link href="#">About</Footer.Link>
<Footer.Link href="#">Privacy Policy</Footer.Link>
<Footer.Link href="#">Licensing</Footer.Link>
<Footer.Link href="#">Contact</Footer.Link>
</Footer.LinkGroup>
</Footer>
</div>
)
}

export default Home

We built the first feature with ethersjs.

Now we want to call our smart contract.

Integrating the Front-End

Generate Typescript code from ABI

Because we use typescript, so we can’t like javascript to directly call ABI.

Recommend TypeChain library. It helps us to create typescript code from Ethereum smart contract.

Install TypeChain

1
2
3
// on ./DApp-Demo
cd front_end
yarn add typechain @typechain/ethers-v5 -D

Use TypeChain

1
2
3
4
// on ./DApp-Demo/front_end
// Keep you already built the smart contract before.
npx typechain --target ethers-v5 --out-dir generated/contract-types '../chain_end/out/Counter.sol/Counter.json'
Output: Successfully generated 5 typings!

Now, we finished from the JSON ABI files to the typescript module.

Update the homepage

We changed our front_end/pages/index.tsx file.
Only one important thing is COUNTER_ADDRESS value. It muse be your smart contract deployed address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import type { NextPage } from 'next'
import React from 'react'
import { useState, useEffect } from 'react'
import { ethers } from 'ethers'
import { Counter__factory } from '../generated/contract-types'
import { Navbar, Footer, Card, Label, TextInput, Button } from 'flowbite-react'
declare let window: any

const Home: NextPage = () => {
const [address, setAddress] = useState<string>()
const [balance, setBalance] = useState<string>()
const [count, setCount] = useState<number>(0)
const [number, setNumber] = useState<number>(0)
const [time, setTime] = useState(Date.now())
const COUNTER_ADDRESS = '0x4D32EEaee44e11cBD3fDE99F38f0885D0F735dE3'

useEffect(() => {
const interval = setInterval(() => setTime(Date.now()), 5000)
return () => {
clearInterval(interval)
}
}, [])

useEffect(() => {
const provider = new ethers.providers.StaticJsonRpcProvider()
const counter = Counter__factory.connect(COUNTER_ADDRESS, provider)
if (counter) {
counter.number().then((count) => {
setCount(count.toNumber())
})
}
}, [time])

const handleConnectWallet = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum)
await provider.send('eth_requestAccounts', [])
const signer = provider.getSigner()
setAddress(await signer.getAddress())
setBalance(ethers.utils.formatEther(await signer.getBalance()))
}

const handleRefresh = async () => {
const provider = new ethers.providers.StaticJsonRpcProvider()
const counter = Counter__factory.connect(COUNTER_ADDRESS, provider)
const n = await counter.number()
setCount(n.toNumber())
}

const handleIncrement = async () => {
console.log('increment')
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner()
const counter = Counter__factory.connect(COUNTER_ADDRESS, signer)
await counter.increment()
}

const handleSetNumber = async () => {
console.log('set number')
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner()
const contract = Counter__factory.connect(COUNTER_ADDRESS, signer)
await contract.setNumber(number)
}

return (
<div className="">
<Navbar fluid={true} rounded={true}>
<Navbar.Brand href="/">
<img
src="https://flowbite.com/docs/images/logo.svg"
className="mr-3 h-6 sm:h-9"
alt="Flowbite Logo"
/>
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
DApp Demo
</span>
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
{address ? (
<>
<div>{address}</div>
<div>{balance}</div>
</>
) : (
<Button onClick={handleConnectWallet}>Connect Wallet</Button>
)}
</Navbar.Collapse>
</Navbar>
<div className="min-w-full min-h-full">
<div className="container flex flex-col justify-center items-center space-y-5">
<div className="text-3xl font-bold">Counter {count}</div>
<Button color="light" onClick={handleRefresh}>
Refresh Counter
</Button>

<Card>
<Button onClick={handleIncrement}>Increment Counter</Button>
</Card>

<Card>
<div>
<div className="mb-2 block">
<Label htmlFor="number" value="Set Number" />
</div>
<TextInput
id="number"
type="number"
placeholder="Enter number"
value={number}
required={true}
onChange={(e) => setNumber(parseInt(e.target.value))}
/>
</div>

<Button type="submit" onClick={handleSetNumber}>
Submit
</Button>
</Card>
</div>
</div>
<Footer container={true}>
<Footer.Copyright
href="#"
by="OhMyApps™"
year={new Date().getFullYear()}
/>
<Footer.LinkGroup>
<Footer.Link href="#">About</Footer.Link>
<Footer.Link href="#">Privacy Policy</Footer.Link>
<Footer.Link href="#">Licensing</Footer.Link>
<Footer.Link href="#">Contact</Footer.Link>
</Footer.LinkGroup>
</Footer>
</div>
)
}

export default Home

Nice work!

2022-10-23 00.15.00.gif

Deploying it to the World

So, we have it working great locally, how do we get it out to the world?

Deploying smart contract to Goerli test network

Get some test GoEth

I really recommend this faucet https://goerli-faucet.pk910.de/. It doesn’t register any account. It is easy.

Get the goerli RPC url

sign up the https://infura.io and you can find it.

It looks like this:
https://goerli.infura.io/v3/YOUR_API_KEY

Get the etherscan api key to verify

https://etherscan.io/myapikey here find your api key.

The smart contract deploys to Goerli test network

1
2
3
// on ./DApp-Demo
cd chain_end
forge create --rpc-url https://goerli.infura.io/v3/YOUR_API_KEY --private-key YOUR_PRIVATE_KEY src/Counter.sol:Counter --etherscan-api-key YOUR_ETHERSCAN_API_KEY --verify

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[⠆] Compiling...
No files changed, compilation skipped
Deployer: 0x858F72b2919d5A5794bE7b93ce4e603f8FFB8792
Deployed to: 0x4B54941BB18D54dD78D9bA598bc799a759c671c9
Transaction hash: 0x736ac18b04092b9acf3d4140ae49d7e93e450a49f14fe96081ed5e4fe26f38cc
Starting contract verification...
Waiting for etherscan to detect contract deployment...

Submitting verification for [src/Counter.sol:Counter] "0x4B54941BB18D54dD78D9bA598bc799a759c671c9".

Submitting verification for [src/Counter.sol:Counter] "0x4B54941BB18D54dD78D9bA598bc799a759c671c9".

Submitting verification for [src/Counter.sol:Counter] "0x4B54941BB18D54dD78D9bA598bc799a759c671c9".

Submitting verification for [src/Counter.sol:Counter] "0x4B54941BB18D54dD78D9bA598bc799a759c671c9".

Submitting verification for [src/Counter.sol:Counter] "0x4B54941BB18D54dD78D9bA598bc799a759c671c9".
Submitted contract for verification:
Response: `OK`
GUID: `ukrqjb5n3rmrxbns2jf91a2nbbmiqkjv8vticjhti3t4dzw6jf`
URL:
https://goerli.etherscan.io/address/0x4b54941bb18d54dd78d9ba598bc799a759c671c9
Waiting for verification result...
Contract successfully verified

Please remember this is deployed to value. It is your smart contract address.

Checks it on Etherscan

Our code was verified. Nice!

image.png

Deploying website to IPFS

Update our front-end code

We need to do two things:

  1. replace StaticJsonRpcProvider with InfuraProvider
  2. Uses the correct smart contract address. replace const COUNTER_ADDRESS value.

We changed our front_end/pages/index.tsx file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import type { NextPage } from 'next'
import React from 'react'
import { useState, useEffect } from 'react'
import { ethers } from 'ethers'
import { Counter__factory } from '../generated/contract-types'
import {
Navbar,
Footer,
Card,
Label,
TextInput,
Button,
Alert,
} from 'flowbite-react'
declare let window: any

const Home: NextPage = () => {
const [address, setAddress] = useState<string>()
const [balance, setBalance] = useState<string>()
const [count, setCount] = useState<number>(0)
const [number, setNumber] = useState<number>(0)
const [time, setTime] = useState(Date.now())
const COUNTER_ADDRESS = '0x4B54941BB18D54dD78D9bA598bc799a759c671c9'

useEffect(() => {
const interval = setInterval(() => setTime(Date.now()), 5000)
return () => {
clearInterval(interval)
}
}, [])

useEffect(() => {
const provider = new ethers.providers.InfuraProvider('goerli')
const counter = Counter__factory.connect(COUNTER_ADDRESS, provider)
if (counter) {
counter.number().then((count) => {
setCount(count.toNumber())
})
}
}, [time])

const handleConnectWallet = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum)
await provider.send('eth_requestAccounts', [])
const signer = provider.getSigner()
setAddress(await signer.getAddress())
setBalance(ethers.utils.formatEther(await signer.getBalance()))
}

const handleRefresh = async () => {
const provider = new ethers.providers.InfuraProvider('goerli')
const counter = Counter__factory.connect(COUNTER_ADDRESS, provider)
const n = await counter.number()
setCount(n.toNumber())
}

const handleIncrement = async () => {
console.log('increment')
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner()
const counter = Counter__factory.connect(COUNTER_ADDRESS, signer)
await counter.increment()
}

const handleSetNumber = async () => {
console.log('set number')
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner()
const contract = Counter__factory.connect(COUNTER_ADDRESS, signer)
await contract.setNumber(number)
}

return (
<div className="">
<Navbar fluid={true} rounded={true}>
<Navbar.Brand href="/">
<img
src="https://flowbite.com/docs/images/logo.svg"
className="mr-3 h-6 sm:h-9"
alt="Flowbite Logo"
/>
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
DApp Demo
</span>
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
{address ? (
<>
<div>{address}</div>
<div>{balance}</div>
</>
) : (
<Button onClick={handleConnectWallet}>Connect Wallet</Button>
)}
</Navbar.Collapse>
</Navbar>

<Alert
color="warning"
additionalContent={
<>
<a
href={`https://goerli.etherscan.io/address/${COUNTER_ADDRESS}`}
className="mr-2 inline-flex items-center rounded-lg bg-yellow-700 px-3 py-1.5 text-center text-xs font-medium text-white hover:bg-yellow-800 focus:ring-4 focus:ring-yellow-300 dark:bg-yellow-800 dark:hover:bg-yellow-900"
>
View more
</a>
</>
}
>
<div className="flex w-full justify-between flex-row space-x-5">
<div>
<span className="font-medium">Alert!</span> This smart contract runs
on the Goerli testnet.
</div>
</div>
</Alert>

<div className="min-w-screen min-h-full mx-auto mt-8">
<div className="container flex flex-col justify-center items-center space-y-5">
<div className="text-3xl font-bold">Counter {count}</div>
<Button color="light" onClick={handleRefresh}>
Refresh Counter
</Button>

<Card>
<Button onClick={handleIncrement}>Increment Counter</Button>
</Card>

<Card>
<div>
<div className="mb-2 block">
<Label htmlFor="number" value="Set Number" />
</div>
<TextInput
id="number"
type="number"
placeholder="Enter number"
value={number}
required={true}
onChange={(e) => setNumber(parseInt(e.target.value))}
/>
</div>

<Button type="submit" onClick={handleSetNumber}>
Submit
</Button>
</Card>
</div>
</div>
<Footer container={true}>
<Footer.Copyright
href="#"
by="OhMyApps™"
year={new Date().getFullYear()}
/>
<Footer.LinkGroup>
<Footer.Link href="#">About</Footer.Link>
<Footer.Link href="#">Privacy Policy</Footer.Link>
<Footer.Link href="#">Licensing</Footer.Link>
<Footer.Link href="#">Contact</Footer.Link>
</Footer.LinkGroup>
</Footer>
</div>
)
}

export default Home

Update front end project config

  1. Open package.json and add the following scripts
1
2
3
4
5
6
7
8
...
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"export": "next export"
}
...
  1. Open next.config.js and add two lines of code.
1
2
3
4
5
6
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
exportTrailingSlash: true,
assetPrefix: './',
}

Deploys front end project to IPFS

We will use the fleek to help us to deploy our front end project.

The fleek likes vercel. They are the same and easy to use. Just the fleek pushes the file to IPFS.

So First we sign up the fleek or sign in if you already have an account.

Then,

  1. Create a new site

image.png

  1. Connect to Github

  2. Pick a repository

image.png

  1. Deploy location

image.png

  1. Build options, and deploy!

Framework selects Next.JS

image.png

Build command
npm install && npm run build && npm run export
changes to

cd front_end && npm install && npm run build && npm run export

Publish directory
out
changes to
front_end/out

image.png

All right. Wait a few minutes.

You will see like:
image.png

We can click Verify on IPFS to verify our website on IPFS.

This is url likes https://ipfs.fleek.co/ipfs/QmcNoHXxrpaxyxKhLhpUoFDzzycvx7o2fUh9mXiG83Xph4/

The QmcNoHXxrpaxyxKhLhpUoFDzzycvx7o2fUh9mXiG83Xph4 is your unique token.

We can replace the URL to https://ipfs.io/ipfs/QmcNoHXxrpaxyxKhLhpUoFDzzycvx7o2fUh9mXiG83Xph4/ for 100% verification on IPFS.

Everything looks great.

Summary

So exciting, We build a real 100% decentralized application from 0 to 1.

We used all modern tech stack:

  1. NextJS
  2. Typescript
  3. TailwindCSS
  4. Solidity
  5. Foundry
  6. IPFS

We deploy a smart contract on Goerli test network and deploy a website on IPFS.

You can find the code on github and visit the website on custom domain or IPFS domain

Demo

image.png

P.S.

This article is very subjective. If you do not feel comfortable viewing it, please close it as soon as possible.
If you think my article can help you, you can subscribe to this site by using RSS.

Referrals

Photo by GuerrillaBuzz Crypto PR on Unsplash

Zero-to-Hero Full-Stack DApp Development in 10 Minutes

Deploying - Foundry Book

What is the difference between a transaction and a call?

Providers & Signers

Providers

Signers

How to deploy a Next.js app onto IPFS using Fleek

Example of Next.js app deployable to IPFS

Zero to One Full-Stack DApp Ethereum Development based on Foundry, NextJS, Typescript - Latest Version

https://iiiyu.com/2022/10/24/Zero-to-One-Full-Stack-DApp-Ethereum-Development-based-on-Foundry-NextJS-Typescript-Latest-Version/

Author

Ewan Xiao

Posted on

October 24th 2022

Updated on

September 28th 2023

Licensed under

Comments