Day 29. Frontend Development for Web 3.0 – Frontend Development –

In day 28, we set up the development environment. In this tutorial, we will finally start developing the frontend for Web 3.0.

目次

File Structure

From the structure mentioned on day 28, the files mainly edited for page development are as follows:

Zsh
.
├── components/*
    ├── Fund.tsx
    └── Header.tsx
...
├── constants/*
    ├── abi.json
    ├── contractAddress
    └── index.ts
...
├── pages/*
    ...
    ├── _app.tsx
    └── index.tsx

A logical representation of the file and directory structure under components and pages would look like this:

_app.tsx is a special file in Next.js used for managing the common layout or state of all pages. It functions as a wrapper for the application, and each page is loaded through this file (global CSS, styles/globals.css is also loaded in this file).

Files under pages are called page components, responsible for displaying pages and handling routing. For example, a file named pages/index.tsx can be accessed at /, and a file named pages/about.tsx can be accessed at /about.

Files under components are for storing UI parts used in pages. It’s common to call (import) these files within page components.

In the case of the above files, index.tsx defines the top page, calling two components: Header and Fund.

Files under constants are used for transactions with the blockchain network. Specifically, they define the address of the deployed contract and the contract’s interface (ABI).

Program

pages/_app.tsx

TypeScript
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { MoralisProvider } from "react-moralis";
import { NotificationProvider } from "web3uikit";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <MoralisProvider initializeOnMount={false}>
      <NotificationProvider>
        <Component {...pageProps} />
      </NotificationProvider>
    </MoralisProvider>
  );
}
  • App({ Component, pageProps }: AppProps): This function applies common components and settings for all pages in Next.js. Components for each page are passed as Component, and their props as pageProps.
  • <MoralisProvider initializeOnMount={false}>: This wraps the entire application with a provider that offers the context for using Moralis services. It enables child components to use Moralis’s features (like executing contract functions). Normally, Moralis services are used with authentication credentials, but since we do not intend to use Moralis services themselves, we use initializeOnMount={false} to avoid initial setup.
  • <NotificationProvider>: Wraps the entire application with a notification provider, allowing child components to use notification features. Child components will use this to send notifications when transactions are successfully completed.
  • <Component {...pageProps} />: Renders the actual page component (e.g., index.tsx or about.tsx). Props for each page are passed through pageProps.

※ Props, simply put, are used to pass data from parent components to child components. The code example is as follows:

TypeScript
// Code Example
// Parent Component
function ParentComponent() {
  return <ChildComponent greeting="Hello, World!" />;
}

// Child Component
function ChildComponent(props) {
  return <h1>{props.greeting}</h1>;
}

pages/index.tsx

TypeScript
import Head from "next/head"
import Header from "@/components/Header"
import Fund from "@/components/Fund"

export default function Home() {
    return (
        <>
            <Head>
                <title>Web3 Crowdfunding</title>
                <meta name="description" content="Web3 Crowdfunding" />
                <meta
                    name="viewport"
                    content="width=device-width, initial-scale=1"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <Header />
            <br />
            <Fund />
        </>
    )
}

In index.tsx, a component named Home is defined. Here, Head, Header, and Fund components are used.

  • Head: A special component provided by Next.js that allows adding elements inside the <head> tag.
  • Header, Fund: Individually defined components (next section).

The overall component structure is wrapped within React Fragments (<> and </>). This syntax is used to return multiple elements without a single parent element.

components/Header.tsx

TypeScript
import { ConnectButton } from "web3uikit"

export default function Header() {
    return (
        <div className="p-5 border-b-2 flex flex-row">
            <h1 className="py-4 px-4 font-blog text-3xl">
                Web3 Crowdfunding
            </h1>
            <div className="ml-auto py-2 px-4">
                <ConnectButton moralisAuth={false} />
            </div>
        </div>
    )
}

Here, the Header component is defined. As mentioned earlier, it is used in the top page (/) component pages/index.tsx. Inside, it uses the ConnectButton defined in web3uikit.

web3uikit is a library that defines UI components that can be used in frontend development for web3. The UI components defined in web3uikit can be checked from this page.

※ The aforementioned link leads to something called Storybook, which is a feature that allows you to manage UI components in a catalog format. It enables the development and testing of individual components in isolation from the application’s overall context.

components/Fund.tsx

The Fund component implements functionalities that allow displaying investor addresses and investment amounts, and enable users to invest and withdraw. At the beginning, it imports necessary functionalities, and inside the main Fund function, it defines interfaces for the variables to be used. The Fund function is explained in six parts, A to F.

TypeScript
import { useMoralis, useWeb3Contract } from "react-moralis"
import { abi, contractAddresses } from "../constants"
import { useEffect, useState } from "react"
import { ContractTransaction, ethers } from "ethers"
import { Button, Input, useNotification } from "web3uikit"

interface contractAddressInterface {
    [key: string]: string[]
}

export default function Fund() {
    // A. Variable Definition
    const addresses: contractAddressInterface = contractAddresses
    const { chainId: chainIdHex, isWeb3Enabled, user, account } = useMoralis()
    const chainId = parseInt(chainIdHex!).toString()
    const fundingAddress = chainId in addresses ? addresses[chainId][0] : null

    //// state
    const [owner, setOwner] = useState("0")
    const [funder, setFunder] = useState("0")
    const [index, setIndex] = useState("0")
    const [addressToAmountFunded, setAddressToAmountFunded] = useState("0")

    const dispatch = useNotification()
    const userAddress = account ? ethers.getAddress(account!) : ""

    // B. Contract Function Definition
    const {
        runContractFunction: fund,
        isLoading,
        isFetching,
    } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "fund",
        params: {},
        msgValue: 110000000000000000,
    })

    const { runContractFunction: withdraw } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "withdraw",
        params: {},
    })

    const { runContractFunction: getOwner } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "getOwner",
        params: {},
    })

    const { runContractFunction: getFunder } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "getFunder",
        params: { index: index },
    })

    const { runContractFunction: getAddressToAmountFunded } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "getAddressToAmountFunded",
        params: { funder: funder },
    })

    // C. Updating State Variables on Screen
    async function updateUI() {
        const ownerFromCall = (await getOwner()) as string
        const funderFromCall = (await getFunder()) as string
        const addressToAmountFundedFromCall = String(
            (await getAddressToAmountFunded()) as BigInt,
        )
        setOwner(ownerFromCall)
        setFunder(funderFromCall)
        setAddressToAmountFunded(addressToAmountFundedFromCall)
    }

    // D. Notification Display on Successful Transaction
    const handleSuccess = async function (txn: ContractTransactionResponse) {
        await txn.wait(1)
        handleNewNotification()
        updateUI()
    }
    const handleNewNotification = function () {
        dispatch({
            type: "info",
            message: "Transaction is successful!",
            title: "Nortification",
            position: "topR",
        })
    }

    // E. React's useEffect Hooks
    useEffect(() => {
        if (isWeb3Enabled) {
            updateUI()
        }
    }, [isWeb3Enabled, index, addressToAmountFunded])

    // F. Display Screen
    return (
        <div>
            {fundingAddress ? (
                <div className="p-5">
                    <div>Contract owner: {owner}</div>
                    <br />
                    Please enter the funder's index number (0, 1...).
                    <div className="p-5">
                        <Input
                            label="Funder's Index Number"
                            name="Funder's Index Number"
                            type="number"
                            onChange={(event) => {
                                setIndex(event.target.value)
                            }}
                        />
                        <br />
                        <div>Funder's address: {funder}</div>
                        <div>Funded amount: {addressToAmountFunded}</div>
                    </div>
                    To fund, please press the following button.
                    <br />
                    <Button
                        text="Fund"
                        theme="outline"
                        onClick={async function () {
                            await fund({
                                onSuccess: (txn) =>
                                    handleSuccess(txn as ContractTransactionResponse),
                                onError: (error) => console.log(error),
                            })
                        }}
                    ></Button>
                    <br />
                    {userAddress == owner ? (
                        <div>
                            The contract owner can press the following button to withdraw the contract balance.
                            <br />
                            <Button
                                text="Withdraw"
                                theme="outline"
                                onClick={async function () {
                                    await withdraw({
                                        onSuccess: (txn) =>
                                            handleSuccess(
                                                txn as ContractTransactionResponse,
                                            ),
                                        onError: (error) => console.log(error),
                                    })
                                }}
                            ></Button>
                        </div>
                    ) : (
                        <div>
                            The connected address is not the contract owner.
                        </div>
                    )}
                </div>
            ) : (
                <div> Contract address information not found.</div>
            )}
        </div>
    )
}

A. Variable Definition

TypeScript
const { chainId: chainIdHex, isWeb3Enabled, account } = useMoralis()

This calls the useMoralis hook and destructures information obtained from Moralis. Specifically:

  • chainIdHex: The ID of the blockchain network currently connected to. This is converted from hexadecimal to a decimal string on line 15.
  • isWeb3Enabled: A boolean value indicating whether Web3 is enabled (whether the user viewing the screen has connected a wallet).
  • account: The Ethereum address of the currently connected user.

※ The values that can be obtained with the useMoralis hook are defined in this table.

TypeScript
const fundingAddress = chainId in addresses ? addresses[chainId][0] : null

This uses a ternary operator to check whether chainId exists as a key in the addresses object.

  • If it exists, it retrieves addresses[chainId][0] and assigns it to fundingAddress. This seems to represent the first contract address for a specific network.
  • If it doesn’t exist, fundingAddress becomes null.

The values of the addresses object would be like 31337: ['0x5FbDB2315678afecb367f032d93F642f64180aa3'] (for localhost only, refer to the constants section mentioned later).

Lines 19-22 use React’s useState to define four state variables: owner, funder, index, addressToAmountFunded. Each state variable has an associated function (setOwner, setFunder, etc.) to change its state. When the value of these state variables changes, the component using them re-renders.

TypeScript
const [index, setIndex] = useState("0") 

For example, index represents the investor’s index number (indicating their sequence in investment), and it is updated every time a new number is entered by the user, triggering setIndex. While index is not used for display on the screen, each time a new index is input, useEffect is executed (explained in E), updating accompanying information like the investment amount and reflecting it on the screen.

Line 24 defines a function for notifying the user using the features of web3uikit. This is used in D, and in this example, it displays a message at the top right of the screen as an info-level notification, indicating that the transaction was completed successfully.

Line 25 retrieves the address (of the user connected to this frontend) from the account information.

TypeScript
const userAddress = account ? ethers.getAddress(account!) : ""

B. Defining Contract Functions

We’re preparing to execute functions of a smart contract against the blockchain network using the Moralis hook, useWeb3Contract. The following example retrieves the address of an investor from their index number.

TypeScript
    const { runContractFunction: getFunder } = useWeb3Contract({
        abi: abi,
        contractAddress: fundingAddress!,
        functionName: "getFunder",
        params: { index: index },
    })

{ runContractFunction: getFunder }: This extracts the function named runContractFunction from the object returned by the useWeb3Contract hook, renaming it as getFunder.

The following object is passed as an argument to useWeb3Contract:

  • abi: ABI (Application Binary Interface), a JSON format data that defines the methods and structure of a smart contract.
  • contractAddress: The address of the smart contract being used. The ! in fundingAddress! tells TypeScript that this variable is not null or undefined.
  • functionName: The name of the function in the smart contract to be executed. In this case, the function “getFunder” is specified.
  • params: An object specifying the arguments for executing the smart contract function. Here, the argument is passed with the name index.

In useWeb3Contract, if the corresponding function in the smart contract returns any value, that value becomes the result of this execution.

These functions are then used in the next section, C.

C. Updating State on the Screen

In C, each time the updateUI function is called, the function executes the smart contract function (the process in B), and the obtained values are set within C (such as setFunder) and updated on the screen through the useState mechanism.

TypeScript

    async function updateUI() {
        const ownerFromCall = (await getOwner()) as string
        const funderFromCall = (await getFunder()) as string
        const addressToAmountFundedFromCall = String(
            (await getAddressToAmountFunded()) as BigInt,
        )
        setOwner(ownerFromCall)
        setFunder(funderFromCall)
        setAddressToAmountFunded(addressToAmountFundedFromCall)
    }

D. Notification Display at the End of a Transaction

TypeScript
    const handleSuccess = async function (txn: ContractTransactionResponse) {
        await txn.wait(1)
        handleNewNotification()
        updateUI()
    }
    const handleNewNotification = function () {
        dispatch({
            type: "info",
            message: "Transaction is successful!",
            title: "Notification",
            position: "topR",
        })
    }

When user actions on the screen for F, such as investing or withdrawing (the smart contract functions fund or withdraw defined in B), are executed and successful, the above-mentioned handleSuccess is called. handleSuccess takes as an argument the result of the transaction executed by the smart contract function (ContractTransactionResponse).

await txn.wait(1) waits for a block to be mined and incorporated into the blockchain, and only proceeds to the next step once the transaction is confirmed. In this case, after investing with fund, for example, and waiting for it to be incorporated into the blockchain, it is considered successful, and the handleNewNotification function is executed (as previously mentioned, when executed via the web3uikit provided hook, this function displays a mini-window at the top right of the screen indicating successful transaction completion).

※ Details on ContractTransactionResponse and the wait function are explained here. Depending on the version of ethers.js, it may be a different class (ContractTransaction). If you can’t find the corresponding class, inferring the relevant class from keywords like wait or confirmation in such documentation is a useful approach.

E. React’s useEffect Hooks

TypeScript
    useEffect(() => {
        if (isWeb3Enabled) {
            updateUI()
        }
    }, [isWeb3Enabled, index, addressToAmountFunded])

The useEffect hook monitors the second argument (dependency array) and executes the process in the first argument when these values change. In other words, when the value of isWeb3Enabled is initially read and if it’s true (meaning the wallet is connected to this site), the updateUI function is executed, the screen is displayed, and afterwards, when the values of index or addressToAmountFunded are updated, updateUI is executed again.

F. Display Screen

Section F describes the process for displaying the screen. The following type of screen is displayed (the top title and the part where the address is written are defined in Header.tsx. The part below that is what’s defined in section F).

In section F, along with regular HTML, some components from web3uikit are used. Specifically, the Input and Button components.

TypeScript
<Input
    label="Funder's Index Number"
    name="Funder's Index Number"
    type="number"
    onChange={(event) => {
        setIndex(event.target.value)
    }}
/>

The Input component has a label, name, type (possible values), and an onChange, which is a logic part executed in conjunction with the user’s action. It executes the process inside onChange every time the user’s input value changes. Here, setIndex is executed (as a result, index changes, and since useEffect is monitoring index, it executes, running updateUI, which then executes getOwner, getFunder, etc., essentially updating all investor-related information following the change in the Index value).

TypeScript
<Button
    text="Fund"
    theme="outline"
    onClick={async function () {
        await fund({
            onSuccess: (txn) =>
                handleSuccess(
                    txn as ContractTransactionResponse,
                ),
            onError: (error) => console.log(error),
        })
    }}
>

In the Button component, in addition to text and theme (appearance), there is an onClick. Here too, logic is defined to be executed when the user clicks. Specifically, the fund function defined in B (or withdraw in another section) is executed. If the transaction is successful, onSuccess is called, and in the case of an error, onError is executed. When onSuccess occurs, it calls the function from D to inform the user on the UI of the successful completion of the transaction.

Main Use Case Flow

Entering the Investor’s Index Number

  • The user enters the investor’s index number in F, and setIndex is executed.
  • E monitors the value of index, so when it’s updated, updateUI is executed.
  • In C, getOwner, getFunder, getAddressToAmountFunded are executed (contract functions), and new values are set with setOwner, setFunder, setAddressToAmountFunded.
  • The display of the investor’s address and investment amount in F is updated.

Investing (or Withdrawing)

  • The user presses the “Invest” button in F, and the fund (contract function) is executed.
  • F receives the execution result, and if successful, handleSuccess is executed.
  • In D, user notification is executed, followed by the execution of updateUI.
  • In C, getOwner, getFunder, getAddressToAmountFunded are executed (contract functions), and new values are set with setOwner, setFunder, setAddressToAmountFunded.
  • The display of the investor’s address and investment amount in F is updated.

constants

Manual Output of abi.json and contractAddresses.json

abi and contractAddresses define the interface of the contract (what functions it has and the format of inputs and outputs for each function) and the addresses of the deployed contracts, respectively. These can be manually copied or created in JSON format. Specifically, the abi can be copied from the abi section of artifacts/contracts/Funding.sol/Funding.json, and the contractAddresses can be created using the address displayed in the console during deployment and the Chain ID of the blockchain network where it was deployed.

Automatic Output of abi.json and contractAddresses.json

It is also possible to automatically output these files when the contract is deployed using a script on the contract side.

TypeScript
// deploy/99-update-frontend-constants.ts
import fs from "fs"
import {DeployFunction} from "hardhat-deploy/types"
import {HardhatRuntimeEnvironment} from "hardhat/types"

const FRONT_END_ADDRESSES_FILE = "../nextjs-funding/constants/contractAddresses.json"
const FRONT_END_ABI_FILE = "../nextjs-funding/constants/abi.json"

const updateConstants: DeployFunction = async function (
    hre: HardhatRuntimeEnvironment
  ) {
    const { network, ethers } = hre
    const chainId = network.config.chainId ? network.config.chainId!.toString() : "31337"
    
    if (process.env.UPDATE_FRONT_END) {
        console.log("Writing abi and contract addresses to frontend...")
        const funding = await ethers.getContract("Funding")
        const contractAddresses = JSON.parse(fs.readFileSync(FRONT_END_ADDRESSES_FILE, "utf8"))
        if (chainId in contractAddresses) {
            if (!contractAddresses[network.config.chainId!].includes(await funding.getAddress())) {
                contractAddresses[network.config.chainId!].push(await funding.getAddress())
            }
        } else {
            contractAddresses[network.config.chainId!] = [await funding.getAddress()]
        }
        fs.writeFileSync(FRONT_END_ADDRESSES_FILE, JSON.stringify(contractAddresses))
        fs.writeFileSync(FRONT_END_ABI_FILE, funding.interface.formatJson())
        console.log("abi and contract addresses written!")
    }
}
export default updateConstants
updateConstants.tags = ["all", "frontend"]

Lines 6 and 7 define the paths to files in the constants folder on the frontend side. Specifically, contractsAddresses.json, which saves the contract addresses for each Chain ID, and abi.json, which saves the ABI. For the first run, create empty JSON files like the following.

JSON
{}

On line 18, a JSON format file already containing contract addresses is read and converted into a TypeScript object format (using JSON.parse). Let’s explain each part in more detail:

  • fs: We are using fs (file system), a built-in module of Node.js. This module provides APIs for file operations, such as reading and writing files.
  • fs.readFileSync: The readFileSync function synchronously reads the contents of the specified file. That is, other operations are blocked until the file is fully read.
  • FRONT_END_ADDRESSES_FILE: This constant (or variable) holds the path to the file to be read.
  • “utf8”: The string “utf8” is passed as the second argument to readFileSync. This specifies the file’s encoding, which in this case, means the file is read in UTF-8 encoding.
  • JSON.parse: Converts the content of the file read by readFileSync (assumed to be a JSON format string) into a JavaScript object.

Lines 19 to 25 involve branching depending on whether the file already includes the corresponding chainID, but essentially, it’s about saving data in the format of line 24. await funding.getAddress() retrieves the deployed contract’s address, which is linked to the chainID (31337 in the case of local), making it the value of the element of key 31337 in the contractAddresses array.

Finally, lines 26 and 27 write the target data to their respective files.

  • JSON.stringify(contractAddresses): stringify converts the argument object into a JSON format string.
  • funding.interface.formatJson(): BaseContract.interface.formatJson outputs the ABI as a JSON format string. BaseContract is the basic type for contracts defined in ethers.js. Details about its variables and functions are described in the ethers official documentation. For example, the interface.formatJson function is explained here.

The console display and the execution result were as follows.

Zsh
# console
% yarn hardhat deploy
yarn run v1.22.19
$ /Users/username/code/token-village/funding/node_modules/.bin/hardhat deploy
Nothing to compile
No need to generate any newer typings.
-----------------------------------------------------
Deploying Funding and waiting for confirmations...
deploying "Funding" (tx: 0xbc444ca6c542123b21a0f1a3515a8d8f2f43b980f8e747417596779369851eda)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 538126 gas
Funding deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3
Writing abi and contract addresses to frontend...
abi and contract addresses written!
  Done in 4.96s.
JSON
// nextjs-funding/constants/abi.json
[{"type":"constructor","stateMutability":"undefined","payable":false,"inputs":[]},{"type":"error","name":"Funding__NotEnoughETH","inputs":[]},{"type":"error","name":"Funding__NotOwner","inputs":[]},{"type":"event","anonymous":false,"name":"Funded","inputs":[{"type":"address","name":"funder","indexed":true},{"type":"uint256","name":"fundedAmount","indexed":true}]},{"type":"function","name":"fund","constant":false,"stateMutability":"payable","payable":true,"inputs":[],"outputs":[]},{"type":"function","name":"getAddressToAmountFunded","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"funder"}],"outputs":[{"type":"uint256","name":""}]},{"type":"function","name":"getFunder","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"uint256","name":"index"}],"outputs":[{"type":"address","name":""}]},{"type":"function","name":"getOwner","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"address","name":""}]},{"type":"function","name":"withdraw","constant":false,"payable":false,"inputs":[],"outputs":[]}]
JSON
// nextjs-funding/constatns/contractAddresses.json
{"31337":["0x5FbDB2315678afecb367f032d93F642f64180aa3"]}

index.ts

The actual file contents are called in Fund.tsx, but to ensure correct import, it is defined as follows:

TypeScript
export { default as contractAddresses } from "./contractAddresses.json";
export { default as abi } from "./abi.json";

Errors Related to Nonce

Occasionally, due to the local execution environment, an error message like “Nonce too high. Expected nonce to be x but got x…” may appear. In this case, if using Metamask, it can be resolved by resetting from Settings > Advanced Settings > Clear Activity and Nonce Data.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

After joining IBM in 2004, the author gained extensive experience in developing and maintaining distributed systems, primarily web-based, as an engineer and PM. Later, he founded his own company, designing and developing mobile applications and backend services. He is currently leading a Tech team at a venture company specializing in robo-advisory.

コメント

コメントする

CAPTCHA


目次