DEV Community

Cover image for Build a Web3 app with Solidity
Johnson Chidera
Johnson Chidera

Posted on

Build a Web3 app with Solidity

In this tutorial, I'm going to show you how to build an Ethereum Decentralized application or Dapp with React.js. If you are an absolute beginner, I suggest that you go over to the Ethereum docs for a proper introduction.

What we will be building

We'll be building a Dapp I call SendFunds, you can call it whatever you like. It will be a place where anybody can send ether(Ethereum's native currency) to any address and display the transaction on the front end.

We'll be writing and deploying a smart contract that will let people connect their wallets and interact with our smart contract. We will be deploying our Dapp to the Göerli testnet.

Demo of SendFunds

Installing dependencies

Node js

First we need to install the node package manager. To do this, head over to this website Node.js website.
You can see if you already have node installed by going to your terminal and typing:

node -v
Enter fullscreen mode Exit fullscreen mode

Metamask wallet

We need metamask in order to interact with the ethereum blockchain. Visit this link to install metamask chrome plugin in your chrome browser.

Create-react-app

Next, we will need to install create-react-app which allows us to create and run a react application very easily without too much configuration. You can install it using the following commands:

npm install create-react-app

Enter fullscreen mode Exit fullscreen mode

Sample project

Let's create our react app. Type in the following command in your terminal window.

npx create-react-app send-funds
cd send-funds
npm start
Enter fullscreen mode Exit fullscreen mode

Your browser should open automatically. If it doesn't, head over to your browser and type http://localhost:3000. You should see a webpage like this:

React webpage

Hardhat

Hardhat is a tool that lets us compile our smart contract code quickly and test them locally. It creates a local Ethereum network for us that mimics the actual Ethereum mainnet. How cool is that!
Install Hardhat with the following commands:

npm install --save-dev hardhat

Enter fullscreen mode Exit fullscreen mode

Make sure you are inside the send-funds directory!

Sample project

Let's get a sample project running.
Run:

npx hardhat
Enter fullscreen mode Exit fullscreen mode

Your terminal should look like this:

npx hardhat

Choose the option to “Create a basic sample project”. Say yes to everything. In case you get a conflict error, delete the README.md file in your root directory.

We need a few other dependencies. Let's install them.

npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers moment dotenv 
Enter fullscreen mode Exit fullscreen mode

Writing our smart contract code

Inside of your send-funds folder, navigate to your contracts folder and create a new file called SendFunds.sol. You can go ahead and delete Greeter.sol as we will not be needing it.

I will paste the code we will be needing below and explain what each line does.

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import "hardhat/console.sol";

contract SendFunds {
    constructor() payable {
        console.log("hello");
    }

    event NewTxn(address indexed to, uint256 amount, uint256 timestamp);

    struct SentTransaction {
        address reciever;
        uint256 amount;
        uint256 timestamp;
    }

    SentTransaction[] allTxn;

    function sendFunds(address payable _to, uint256 amount) public payable {
        require(amount <= address(this).balance, "not enough funds");
        (bool success, ) = _to.call{value: amount}("");
        require(success, "Unable to send Ether");
        allTxn.push(SentTransaction(_to, amount, block.timestamp));
        emit NewTxn(_to, amount, block.timestamp);
    }

    function getAllTxn() public view returns (SentTransaction[] memory) {
        return allTxn;
    }
}

Enter fullscreen mode Exit fullscreen mode
//SPDX-License-Identifier: MIT
Enter fullscreen mode Exit fullscreen mode

Every smart contract must begin with an // SPDX-License-Identifier. If you do not do this, an error will occur.

pragma solidity ^0.8.13;
Enter fullscreen mode Exit fullscreen mode

A version of solidity must be indicated next. We do this to tell the compiler to use version 0.8.13. To know more about license identifiers and compiler versions, check this out.

import "hardhat/console.sol";
Enter fullscreen mode Exit fullscreen mode

Hardhat gives us a way to console.log statements to our teminal.

contract SendFunds {
    constructor() payable {
        console.log("hello");
    }
Enter fullscreen mode Exit fullscreen mode

Smart contracts looks like a class in other programming languages. The constructor will run once when the contract is initialized for the first time and print what is in the console.log(). We are making the constructor payable because we want the smart contract to be able to receive ether.

event NewTxn(address indexed to, uint256 amount, uint256 timestamp);
Enter fullscreen mode Exit fullscreen mode

Next is our event. We need to display the transaction on our front end, we need an event to be able to communicate to our front end that some state has changed!

SentTransaction[] allTxn;
Enter fullscreen mode Exit fullscreen mode

struct is used to group related data together. When sending a transaction, we need to store the receiver, the amount and the timestamp.

function sendFunds(address payable _to, uint256 amount) public payable {
Enter fullscreen mode Exit fullscreen mode

Next is our sendFunds function which takes in an address payable _to meaning the address can accept payment. A uint256 amount which takes in the amount to send to _to and the function is payable.

require(amount <= address(this).balance, "not enough funds");
Enter fullscreen mode Exit fullscreen mode

The require keyword returns a true or false. If the first half of it is true, it continues code execution. If false, it throws an error. Here, we are checking if the amount we want to send to the receiver is less than or equal to what the sender has in their wallet.

(bool success, ) = _to.call{value: amount}("");

Enter fullscreen mode Exit fullscreen mode

Above is the magic line that actually sends ether to the receiver. Then we have another require block to check if the transaction was a success.

allTxn.push(SentTransaction(_to, amount, block.timestamp));
emit NewTxn(_to, amount, block.timestamp);
Enter fullscreen mode Exit fullscreen mode

Here, we are pushing _to, amount and block.timestamp to our struct instance and emitting it to the front end.

function getAllTxn() public view returns (SentTransaction[] memory) {
        return allTxn;
    }
Enter fullscreen mode Exit fullscreen mode

For the final block of code, this function above returns all the transactions.

Testing out our smart contract

Before we begin, head over to your hardhat.config.js file and change your version of solidity to 0.8.13 so it would match what you have in your SendFunds.sol file.

In your scripts folder, delete sample-script.js and create two new files. run.js is the first file to create. Here, we would play around with testing different aspects of our code and the next file to create is deploy.js, here is the file we use to deploy our smart contract to your testnet.

The code below should be inside the run.js file.

const hre = require("hardhat");

const main = async () => {
  const sendFundsContractFactory = await hre.ethers.getContractFactory(
    "SendFunds"
  );
  const sendFundsContract = await sendFundsContractFactory.deploy({
    value: hre.ethers.utils.parseEther("4"),
  });

  await sendFundsContract.deployed();

  console.log("contract address: ", sendFundsContract.address);
  let contractBalance = await hre.ethers.provider.getBalance(
    sendFundsContract.address
  );
  console.log(
    "Contract balance:",
    hre.ethers.utils.formatEther(contractBalance)
  );

  const [owner, randomPerson] = await hre.ethers.getSigners();
  const sendFunds = await sendFundsContract
    .connect(randomPerson)
    .sendFunds(randomPerson.address, 2);
  await sendFunds.wait();

  const allTxn = await sendFundsContract.getAllTxn();
  console.log(allTxn);
};

const runMain = async () => {
  try {
    await main();
    process.exit(0);
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

runMain();

Enter fullscreen mode Exit fullscreen mode

Let's go over this line by line.

const hre = require("hardhat");
Enter fullscreen mode Exit fullscreen mode

We are requiring hardhat here because we will be needing it later.

const sendFundsContractFactory = await hre.ethers.getContractFactory(
    "SendFunds"
  );
Enter fullscreen mode Exit fullscreen mode

This will compile our smart contract and generate the necessary files we need to work with our contract under the artifacts folder.

const sendFundsContract = await sendFundsContractFactory.deploy({
    value: hre.ethers.utils.parseEther("4")
  });

Enter fullscreen mode Exit fullscreen mode

Hardhat will create a local Ethereum network for us. Then, after the script completes it'll destroy that local network and we are giving the contract 4 ether.

  await sendFundsContract.deployed();

Enter fullscreen mode Exit fullscreen mode

Here, we are waiting for the contract to be deployed.

console.log("contract address: ", sendFundsContract.address);
  let contractBalance = await hre.ethers.provider.getBalance(
    sendFundsContract.address
  );
  console.log(
    "Contract balance:",
    hre.ethers.utils.formatEther(contractBalance)
  );
Enter fullscreen mode Exit fullscreen mode

Next, we are console.logging the contract's address and the contract's balance.

const [owner, randomPerson] = await hre.ethers.getSigners();
  const sendFunds = await sendFundsContract
    .connect(randomPerson)
    .sendFunds(randomPerson.address, 2);
  await sendFunds.wait();
Enter fullscreen mode Exit fullscreen mode

What is going on here is we are getting a random user to send some ether to and we are calling the sendFunds function passing in the random user's address and amount and waiting for the transaction to be completed.

const allTxn = await sendFundsContract.getAllTxn();
  console.log(allTxn);

Enter fullscreen mode Exit fullscreen mode

For the final bit of testing, we are calling the getAllTxn function to get all of our transactions.

Run the following command in your terminal: npx hardhat run scripts/run.js. Your terminal should be outputing the following:

test terminal

Let's write our deploy script. It will be very similar to our run.js file.

Input the following in your deploy.js file.

const hre = require("hardhat");

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

    console.log("deploying contracts with account ", deployer.address);
    console.log("account balance ", accountBalance.toString());

    const sendFundsContractFactory = await hre.ethers.getContractFactory("SendFunds");
    const sendFundsContract = await sendFundsContractFactory.deploy();

    await sendFundsContract.deployed();

    console.log("Funds contract address: ", sendFundsContract.address)

    }

    const runMain = async () => {
        try {
            await main();
            process.exit(0)
        } catch (error) {
            console.log(error);
            process.exit(1)
        }
    }

    runMain();
Enter fullscreen mode Exit fullscreen mode

Deploy to the Göerli testnet with Alchemy

We are going to be deploying to a testnet because deploying to the Ethereum Mainnet costs real money. I'll show you how to deploy to a testnet using Alchemy.

After logging in to Alchemy, on the top right corner, there is a create app button. Click on it

Alchemy dashboard

A pop up should appear next. Give your app a name, the chain should be Ethereum and network should be changed to Göerli. Finally click on create app button.

testnet

Next, click on your newly created project, it should take you to your project's dashboard. You will be needing the API url.

View key

API

Next, create a .env file in your root directory. We would be adding some things we don't want to public to get access to like your private key and API url. Don't forget to add your .env file to your gitignore file.

Head over to your hardhat.config.js file. Input the following:

require("@nomiclabs/hardhat-waffle");
require('dotenv').config();

module.exports = {
  solidity: "0.8.13",
  networks: {
    goerli: {
      url: process.env.ALCHEMY_URL,
      accounts: [process.env.WALLET_PRIVATE_KEY],
    },
  }
};

Enter fullscreen mode Exit fullscreen mode

Let's go over this.

require('dotenv').config();
Enter fullscreen mode Exit fullscreen mode

First, we are requiring dotenv

module.exports = {
  solidity: "0.8.13",
  networks: {
    goerli: {
      url: process.env.ALCHEMY_URL,
      accounts: [process.env.WALLET_PRIVATE_KEY],
    },
  }
};
Enter fullscreen mode Exit fullscreen mode

Next, we are filling in the url and accounts with our alchemy API url and our private key. To get your wallet's private key, head over here.
Please keep your private key safe to avoid loss of funds.

Before we deploy to the testnet, we need test Göerli. Head over to Göerli faucet. Login with Alchemy and paste in your wallet address. You should receive your test Göerli in a couple of seconds.

In your terminal, run the following commands to deploy your contract to the Göerli testnet: npx hardhat run scripts/deploy.js --network goerli

Your terminal should output the following:

Deployed successfully

Copy your contract's address. We are going to need it in the front end.

You've come a long way. Now let's connect our front end.

Setting up the front end

Let's start by deleting some unwanted code in your App.js file under your src folder. It should look like this:

import './App.css';

function App() {
  return (
    <div>
      hello
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Next, we are going to be creating some new folders. Under your src folder, create two new folders: components and utils.
Inside your components folder, create two new files: Home.js and Home.css.

Inside of your Home.js file. Input the following code:

import React, { useEffect, useState } from "react";

function Home() {
  const [currentAccount, setCurrentAccount] = useState("");

  const checkIfWalletIsConnected = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        console.log("you need to install metamask");
      } else {
        console.log("found one", ethereum);
      }
      /*
       * Check if we're authorized to access the user's wallet
       */

      const accounts = await ethereum.request({ method: "eth_accounts" });
      if (accounts.length !== 0) {
        const account = accounts[0];
        console.log("account ", account);
        setCurrentAccount(account);
      } else {
        console.log("no authorized account found");
      }
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    checkIfWalletIsConnected();
  }, []);

  return <div>Home</div>;
}

export default Home;

Enter fullscreen mode Exit fullscreen mode

What is going on here is that we are basically checking to see if the special window.ethereum object is injected in our browser. If not, you get a console.log telling you to install Metamask. If the object is found, we use a special method called eth_accounts to see if we're authorized to access any of the accounts in the user's wallet and since a user can have multiple accounts, we are taking the first one. Finally we are using the useEffect hook to run the function immediately the page loads.

Connecting our wallet

Connecting our wallet is very easy to do. Your Home.js file should look like the following:

import React, { useEffect, useState } from "react";
import "./Home.css";

function Home() {
  const [currentAccount, setCurrentAccount] = useState("");

  const checkIfWalletIsConnected = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        console.log("you need to install metamask");
      } else {
        console.log("found one", ethereum);
      }
      /*
       * Check if we're authorized to access the user's wallet
       */

      const accounts = await ethereum.request({ method: "eth_accounts" });
      if (accounts.length !== 0) {
        const account = accounts[0];
        console.log("account ", account);
        setCurrentAccount(account);
      } else {
        console.log("no authorized account found");
      }
    } catch (error) {
      console.log(error);
    }
  };

  //connect wallet with button click
  const connectWallet = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        console.log("you need to install metamask");
        return;
      }
      const accounts = await ethereum.request({
        method: "eth_requestAccounts",
      });

      console.log("Connected", accounts[0]);
      setCurrentAccount(accounts[0]);
    } catch (error) {
      console.log(error);
    }
  };
  useEffect(() => {
    checkIfWalletIsConnected();
  }, []);

  //truncate wallet address
  function truncate(input) {
    return input.substring(0, 5) + "..." + input.substring(38);
  }

  return (
    <div className="App">
      {currentAccount.length === 0 ? (
        <div>
          <div className="nav">
            <h1>SendFunds</h1>
          </div>
          <div className="content">
            <div>
              <p className="description">
                Send <i className="fa-brands fa-ethereum"></i> to your friends
                and family.
              </p>
              <button className="connect-btn" onClick={() => connectWallet()}>
                Connect Wallet
              </button>
            </div>
          </div>
        </div>
      ) : (
        <div>
          <div className="nav flex">
            <h1>SendFunds</h1>
            <p className="wallet-address">{truncate(currentAccount)}</p>
          </div>
          <div className="content connected-wallet">
            <p className="description">
              Send <i className="fa-brands fa-ethereum"></i> to your friends and
              family.
            </p>
          </div>
        </div>
      )}
    </div>
  );
}

export default Home;

Enter fullscreen mode Exit fullscreen mode

Let's go over the connectWallet and truncate functions.

const connectWallet = async () => {
    try {
      const { ethereum } = window;
      if (!ethereum) {
        console.log("you need to install metamask");
        return;
      }
      const accounts = await ethereum.request({
        method: "eth_requestAccounts",
      });

      console.log("Connected", accounts[0]);
      setCurrentAccount(accounts[0]);
    } catch (error) {
      console.log(error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

Here, we are checking if window.ethereum is present. If it is, we call eth_requestAccounts to ask Metamask to give us access to the user's wallet. Then we are setting setCurrentAccount to the first account.

 function truncate(input) {
    return input.substring(0, 5) + "..." + input.substring(38);
  }

Enter fullscreen mode Exit fullscreen mode

Since, wallet addresses are too long, we are truncating it.

Finally, we are doing some conditional rendering. If currentAccount.length === 0, the user needs to connect their wallet, else display a welcome text.

The styling for the Home.css page:

body{
    background: rgb(100,0,123);
    background: radial-gradient(circle, rgba(100,0,123,1) 0%, rgba(62,20,86,1) 100%);
    color: #fff;
    margin: 2px 40px;
    font-family: 'Bellota', cursive;
  }

  .content {
    text-align: center;
    margin: 160px auto 40px;
  }

  .description {
    font-size: 30px;
    font-weight: bold;
  }

  .connect-btn {
  color: white;
  padding: 10px 30px;
  font-size: 1.3em;
  background: transparent;
  border-radius: 50px;
  border: 1px solid white;
  margin: 10px auto 0;
  cursor: pointer;
  opacity: 0.7;
  font-family: 'Bellota', cursive;
  font-weight: bold;
  }

.nav {
  border-bottom: 1px solid #fff;
}

.nav h1 {
  margin-bottom: 0;
  text-align: left;
}

.flex {
  display: flex;
  align-items: center;
  justify-content: space-between;
  place-items: flex-end;
}

.nav p {
  margin: 3px 0;
}

.connected-wallet {
  margin: 70px auto 40px;

}

.wallet-address {
  border: 1px solid #fff;
  padding: 2px 15px;
  border-radius: 50px;
}
Enter fullscreen mode Exit fullscreen mode

I got my icon from font awesome and added the cdn to my index.html file. For the font, I used Bellota from google fonts and also added the link to my index.html file.

Import Home.js in your App.js file.

import './App.css';
import Home from './components/Home';


function App() {
  return (
    <div>
      <Home />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Run npm start to check out your Dapp.

Your Home page should be looking like this:

Home page

Form implementation

Let's dive into our form creation. Under the utils folder, create a new file called SendFunds.json. This will house the artifacts gotten when you deployed your contract.
Under artifacts/contracts/SendFunds.sol, you will find a SendFunds.json file. Copy everything and paste in inside your utils/SendFunds.json.

You also need to create two new files under your components: Form.js and Form.css.

Let's create a custom form inside your Form.js file:

import React, {useState} from 'react';
import './Form.css';


const Form = () => {
    const [walletAddress, setWalletAddress] = useState('')
    const [amount, setAmount] = useState('')


  return (
    <div className="form">
      <form>
        <p>
          <input
            type="text"
            name=""
            id=""
            placeholder="Enter Wallet Address"
            required
            value={walletAddress}
            onChange={(e) => setWalletAddress(e.target.value)}
          />
        </p>
        <p>
          <input
            type="number"
            name=""
            id=""
            placeholder="Enter Amount"
            required
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            step='any'
            min='0'
          />
        </p>
        <button type="submit">
          Send
        </button>
      </form>

    </div>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

The code above is pretty straight forward. Two input fields. One a number and the other a text type. Then, I am saving the values in state.

Note: Don't forget to include your Form.js file at the bottom of your Home.js file.

Now, let's call our sendFunds function from our smart contract.

import React, { useState } from "react";
import { ethers } from "ethers";
import abi from "../utils/SendFunds.json";
import { parseEther } from "ethers/lib/utils";

const Form = () => {
  const [walletAddress, setWalletAddress] = useState("");
  const [amount, setAmount] = useState("");

  const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
  const contractABI = abi.abi;
  const sendFunds = async () => {
    try {
      const { ethereum } = window;
      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const sendFundsContract = new ethers.Contract(
          contractAddress,
          contractABI,
          signer
        );
        const sendFundsTxn = await sendFundsContract.sendFunds(
          walletAddress,
          ethers.utils.parseEther(amount),
          { gasLimit: 300000, value: parseEther(amount) }
        );
        await sendFundsTxn.wait();
        setWalletAddress('')
        setAmount('')
      } else {
        console.log("ethereum object does not exist!");
      }
    } catch (error) {
      console.log(error);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    sendFunds();
  };

  return (
    <div className="form">
      <form onSubmit={handleSubmit}>
        <p>
          <input
            type="text"
            name=""
            id=""
            placeholder="Enter Wallet Address"
            required
            value={walletAddress}
            onChange={(e) => setWalletAddress(e.target.value)}
          />
        </p>
        <p>
          <input
            type="number"
            name=""
            id=""
            placeholder="Enter Amount"
            required
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            step="any"
            min="0"
          />
        </p>
        <button type="submit">Send</button>
      </form>
    </div>
  );
};

export default Form;

Enter fullscreen mode Exit fullscreen mode

We have a lot going on here, so let's break it down.

import { ethers } from "ethers";
Enter fullscreen mode Exit fullscreen mode

We are importing ethers because we will need it in order to interact with our smart contract.

import abi from "../utils/SendFunds.json";
Enter fullscreen mode Exit fullscreen mode

Next, we are importing our abi. You can read more about it here.

import { parseEther } from "ethers/lib/utils";
Enter fullscreen mode Exit fullscreen mode

We use parseEther when we want to convert a value from ETH to WEI which is the value we want to send to the contract when calling a payable method.

const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
Enter fullscreen mode Exit fullscreen mode

The contract address gotten when we deployed our smart contract. Incase you didn't save yours, run npx hardhat run scripts/deploy.js --network goerli.

  const contractABI = abi.abi;
Enter fullscreen mode Exit fullscreen mode

The abi gotten from our SendFunds.json file.

const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
Enter fullscreen mode Exit fullscreen mode

A Provider is what we use to actually talk to Ethereum nodes. A signer is used to sign transactions and send the signed transactions to the Ethereum network. Read more abour signers here.

const sendFundsTxn = await sendFundsContract.sendFunds(
          walletAddress,
          ethers.utils.parseEther(amount),
          { gasLimit: 300000, value: parseEther(amount) }
        );
        await sendFundsTxn.wait();

Enter fullscreen mode Exit fullscreen mode

We are calling the function we wrote in our smart contract and passing in the necessary arguments and waiting for the transaction to execute.

Next is the function to get all of our transactions:

import React, { useEffect, useState } from "react";
import { ethers } from "ethers";
import abi from "../utils/SendFunds.json";
import { parseEther } from "ethers/lib/utils";
import Transaction from "./Transactions";
import "./Form.css";

const Form = () => {
  const [walletAddress, setWalletAddress] = useState("");
  const [amount, setAmount] = useState("");
  const [allTxns, setAllTxns] = useState([]);
  const [isTxn, setIsTxn] = useState(false);

  const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
  const contractABI = abi.abi;
  const sendFunds = async () => {
    try {
      const { ethereum } = window;
      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const sendFundsContract = new ethers.Contract(
          contractAddress,
          contractABI,
          signer
        );
        const sendFundsTxn = await sendFundsContract.sendFunds(
          walletAddress,
          ethers.utils.parseEther(amount),
          { gasLimit: 300000, value: parseEther(amount) }
        );
        await sendFundsTxn.wait();
      } else {
        console.log("ethereum object does not exist!");
      }
    } catch (error) {
      console.log(error);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    sendFunds();
  };

  const getAllTransactions = async () => {
    try {
      const { ethereum } = window;
      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const sendFundsContract = new ethers.Contract(
          contractAddress,
          contractABI,
          signer
        );
        let getAllTxn = await sendFundsContract.getAllTxn();
        setIsTxn(true);

        let txns = [];
        getAllTxn.forEach((txn) => {
          txns.push({
            address: txn.reciever,
            amount: txn.amount,
            timestamp: new Date(txn.timestamp * 1000),
          });
        });
        setAllTxns(txns);
      } else {
        console.log("ethereum object does not exist!");
      }
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    getAllTransactions();
  }, []);

 useEffect(() => {
    let sendFundsContract;

    const onNewTransaction = (to, amount, timestamp) => {
      console.log("New transaction", to, amount, timestamp);
      setAllTxns(prevState => [
        ...prevState,
        {
          address: to,
          amount: amount,
          timestamp: new Date(timestamp * 1000)
        },
      ]);
    };

    if (window.ethereum) {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      sendFundsContract = new ethers.Contract(contractAddress, contractABI, signer);
      sendFundsContract.on("NewTxn", onNewTransaction);
    }

    return () => {
      if (sendFundsContract) {
        sendFundsContract.off("NewTxn", onNewTransaction);
      }
    };
  }, []);

  return (
    <div className="form">
          {/* don't forget to add the input fields, i removed them to make the code shorter */}

      <div>
        {isTxn === false ? (
          <div></div>
        ) : (
          <div>
            <Transaction allTxns={allTxns} />
          </div>
        )}
      </div>
    </div>
  );
};

export default Form;

Enter fullscreen mode Exit fullscreen mode

We are calling the getAllTxn function and pushing it into an array that we store with useState and sending the array to the Transaction component. You can go ahead and create a Transaction.js and Transaction.css files inside your components folder.
I am also catching the event I created on the smart contract so I don't have to refresh the page whenever I send in a new transaction.

The styling I used for the form. Add it to your Form.css file:

* {
  font-family: 'Bellota', cursive;

}

button {
  color: white;
  padding: 10px 30px;
  font-size: 1.3em;
  background: transparent;
  border-radius: 50px;
  border: 1px solid white;
  margin: 10px auto 0;
  cursor: pointer;
  opacity: 0.7;
  font-weight: bold;
}

.form {
    text-align: center;
    margin: 60px auto 40px;
}

input {
    border: 1px solid #fff;
    padding: 8px 13px;
    border-radius: 50px;
    width: 30%;
    margin-bottom: 20px;
    font-weight: bold;
    font-size: 18px;
}
Enter fullscreen mode Exit fullscreen mode

For the final bit of this tutorial, let us display our transactions on the front end.

In your Transaction.js file, input the following code:

import React from 'react';
import './Transaction.css'
import moment from 'moment'
import {ethers} from 'ethers'

const Transaction = ({allTxns}) => {
  console.log(allTxns)
  return (
    <div className='transaction-container'>
    <h2>All Transactions:</h2>
     {allTxns.length === 0 ? <div>

       </div>: <div className='grid-container'>

          {allTxns.map((txn, index) => {
            return (
              <div key={index} className='transactions'>
                <p>Reciever: {txn.address}</p>
                <p>Amount: {ethers.utils.formatUnits(txn.amount.toString(), 'ether')} eth</p>
                <p>Date:  {moment(txn.timestamp.toString()).format('MM/DD/YYYY')}</p>
                </div>
            )
          })}
         </div>}
    </div>
  );
};

export default Transaction;
Enter fullscreen mode Exit fullscreen mode

What is going on here is very clear. We are getting the allTxns prop from the Form.js file and we are displaying the data. Changing WEI to ETH using ethers.utils.formatUnits and changing the txn.timestamp to something more readable using moment.

Styling for Transaction.css file:

.transaction-container {
    text-align: left;
    margin-top: 20px;
}

.grid-container {
    display: grid;
    grid-template-columns: auto auto auto;
    grid-gap: 10px;
}

.transactions{
    background-color: #ffffff;
    color: black;
    padding: 0 10px;
    border-radius: 10px;
    width: 60%;

}

Enter fullscreen mode Exit fullscreen mode

Run npm start in your terminal. Send some transactions. Your web page should be looking like this:

Final result

Further learning

Connect with me on: Twitter || LinkedIn

Top comments (5)

Collapse
 
ankg404 profile image
Aniket Gupta

Hi Johnson! This is an amazing article. Thank you for using Alchemy!

I work with the developer relations team at Alchemy. Would love to hear feedback from you. We'd also love to promote your article on our twitter.

I can be reached at aniket@alchemy.com

Collapse
 
dera_johnson profile image
Johnson Chidera

Thank you. This really means a lot to me. I have sent you a message on twitter. Of course, you can promote my article.

Collapse
 
dera_johnson profile image
Johnson Chidera

Of course. Thank you for your kind words ❤️

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch

Great tutorial! Would you mind giving us access to your repository?

Collapse
 
dera_johnson profile image
Johnson Chidera

Thank you :)

github repo: github.com/derajohnson/send-funds-v2