DEV Community

dhxmo
dhxmo

Posted on

Crowdfunding dApp with Solidity and NextJS

I’m a fairly new developer and I’ve found that learning to code is easiest when I’m building a project so I decided to build one.

I built out a crowd funding site that requires no intermediaries. End users come in and just start interacting with the platform. You can check it out here.

Ok, enough talk let’s get to it.

We start with initializing truffle with my main directory:

truffle init
Enter fullscreen mode Exit fullscreen mode

I like working with ganache appimage, so start that and it whips up a local testnet for us on port 8545.

Go into my truffle.config.js and make the necessary changes

const HDWalletProvider = require('@truffle/hdwallet-provider');
const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();
require('dotenv').config();
const projectID = process.env.PROJECT_ID;
module.exports = {
networks: {
   development: {
      host: "127.0.0.1",     // Localhost (default: none)
      port: 8545,            // Standard Ethereum port (default: none)
      network_id: "*",       // Any network (default: none)
   },
   rinkeby: {
// 1.
      provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${projectID}`),
      network_id: "4", // Rinkeby ID 4
      gas: 4465030,
      gasPrice: 10000000000,
   }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
      solc: {
         version: "0.8.4",      // Fetch exact version from solc-bin (default: truffle's version)
      settings: {          
         optimizer: {
            enabled: true,
            runs: 200
         },
      }
   }
  },
};
Enter fullscreen mode Exit fullscreen mode

I ended up hosting the project on rinkeby testnet so I added the infura endpoints here.

Go to infura dashboard, sign up and set up an endpoint for your project. The instructions are here. Once you have a project set up, go into the settings and copy the project’s ID and paste it into a .env file in the root directory

PROJECT_ID=db0b4735bad24926a761d909e1f82576
Enter fullscreen mode Exit fullscreen mode

since this project is out in the open, I put this here. You don’t want to do this for production stuff. Something to do with compromising security and the lot. I don’t know the exact details why, but delving into security is my next move so someday i’ll write a post and I’ll know why the hell this is unsafe.

the mnemonic is the private key to the account that ends up deploying this to the testnet, so make sure you get some fake ETH from a rinkeby faucet before deploying.

I’ll slap the code here because it’s well documented and self-explanatory:

The github repo is here.

I usually like to define all the events in the contract in the beginning because it gives me a sense of all the things that are going to happen in this contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract CrowdFund is ReentrancyGuard {
    using SafeMath for uint256;
/*===== Events =====*/
    event NewProjectCreated(
        uint256 indexed id,
        address projectCreator,
        string projectTitle,
        string projectDesc,
        uint256 projectDeadline,
        uint256 goalAmount
    );
event ExpireFundraise(
        uint256 indexed id,
        string name,
        uint256 projectDeadline,
        uint256 goal
    );

    event SuccessFundRaise(
        uint256 indexed id,
        string name,
        uint256 projectDeadline,
        uint256 goal
    );

    event SuccessButContinueFundRaise(
        uint256 indexed id,
        string name,
        uint256 projectDeadline,
        uint256 goal
    );
event FundsReceive(
        uint256 indexed id,
        address contributor,
        uint256 amount,
        uint256 totalPledged
    );
event NewWithdrawalRequest(
        uint256 indexed id,
        string description,
        uint256 amount
    );
event GenerateRefund(
        uint256 indexed id,
        address refundRequestUser,
        uint256 refundAmt
    );
event ApproveRequest(uint256 indexed _id, uint32 _withdrawalRequestIndex);
event RejectRequest(uint256 indexed _id, uint32 _withdrawalRequestIndex);
event TransferRequestFunds(
        uint256 indexed _id,
        uint32 _withdrawalRequestIndex
    );
event PayCreator( uint256 indexed _id, uint32 _withdrawalRequestIndex, uint256 _amountTransfered);
event PayPlatform(uint256 indexed _id, uint32 _withdrawalRequestIndex, uint256 _amountTransfered);
Enter fullscreen mode Exit fullscreen mode

Next, we define all the state variables. These are all the things that have a state and will have some sort of transformation during the interaction with the users.

/*===== State variables =====*/
    address payable platformAdmin;
enum State {
        Fundraise,
        Expire,
        Success
    }
enum Withdrawal {
        Allow,
        Reject
    }
struct Project {
        // project ID
        uint256 id;
        // address of the creator of project
        address payable creator;
        // name of the project
        string name;
        // description of the project
        string description;
        // end of fundraising date
        uint256 projectDeadline;
        // total amount that has been pledged until this point
        uint256 totalPledged;
        // total amount needed for a successful campaign
        uint256 goal;
        // number of depositors
        uint256 totalDepositors;
        // total funds withdrawn from project
        uint256 totalWithdrawn;
        // current state of the fundraise
        State currentState;
        // holds URL of IPFS upload
        // string ipfsURL;
    }
struct WithdrawalRequest {
        uint32 index;
        // purpose of withdrawal
        string description;
        // amount of withdrawal requested
        uint256 withdrawalAmount;
        // project owner address
        address payable recipient;
        // total votes received for request
        uint256 approvedVotes;
        // current state of the withdrawal request
        Withdrawal currentWithdrawalState;
        // hash of the ipfs storage
        // string ipfsHash;
        // boolean to represent if amount has been withdrawn
        bool withdrawn;
    }
mapping(address => uint) balances;
// project states
    uint256 public projectCount;
    mapping(uint256 => Project) public idToProject;
    // project id => contributor => contribution
    mapping(uint256 => mapping(address => uint256)) public contributions;
// withdrawal requests
    mapping(uint256 => WithdrawalRequest[]) public idToWithdrawalRequests;
    // project ID => withdrawal request Index
    mapping(uint256 => uint32) latestWithdrawalIndex;
// project id => request number => address of contributors
    mapping(uint256 => mapping(uint32 => address[])) approvals;
    mapping(uint256 => mapping(uint32 => address[])) rejections;
Enter fullscreen mode Exit fullscreen mode

Next, we put some modifiers in to put strict limitations on who can interact with the contract functions.

/*===== Modifiers =====*/
    modifier checkState(uint256 _id, State _state) {
        require(
            idToProject[_id].currentState == _state,
            "Unmatching states. Invalid operation"
        );
        _;
    }
modifier onlyAdmin() {
        require(
            msg.sender == platformAdmin,
            "Unauthorized access. Only admin can use this function"
        );
        _;
    }
modifier onlyProjectOwner(uint256 _id) {
        require(
            msg.sender == idToProject[_id].creator,
            "Unauthorized access. Only project owner can use this function"
        );
        _;
    }
modifier onlyProjectDonor(uint256 _id) {
        require(
            contributions[_id][msg.sender] > 0,
            "Unauthorized access. Only project funders can use this function."
        );
        _;
    }
modifier checkLatestWithdrawalIndex(
        uint256 _id,
        uint32 _withdrawalRequestIndex
    ) {
        require(
            latestWithdrawalIndex[_id] == _withdrawalRequestIndex,
            "This is not the latest withdrawal request. Please check again and try later"
        );
        _;
    }
Enter fullscreen mode Exit fullscreen mode

I added a reentrancy guard cuz I just don’t want people to be doing greed shit when it comes to this public good.

The fallback for this contract is the platform admin. If there’s some ethers floating around, might as well send it to the admin than go to waste sitting in the contract.

constructor() ReentrancyGuard() {
        platformAdmin = payable(msg.sender);
        projectCount = 0;
    }
// make contract payable
    fallback() external payable {}
    receive() external payable {
        platformAdmin.transfer(msg.value);
    }
/*===== Functions  =====*/
/** @dev Function to start a new project.
     * @param _name Name of the project
     * @param _description Project Description
     * @param _projectDeadline Total days to end of fundraise
     * @param _goalEth Project goal in ETH
     */
    function createNewProject(
        string memory _name,
        string memory _description,
        uint256 _projectDeadline,
        uint256 _goalEth
    ) public {
        // update ID
        projectCount += 1;
        // log goal as wei
        uint256 _goal = _goalEth * 1e18;
        // create new fundraise object
        Project memory newFR = Project({
            id: projectCount,
            creator: payable(msg.sender),
            name: _name,
            description: _description,
            projectDeadline: _projectDeadline,
            totalPledged: 0,
            goal: _goal,
            currentState: State.Fundraise,
            totalDepositors: 0,
            totalWithdrawn: 0
        });
        // update mapping of id to new project
        idToProject[projectCount] = newFR;
        // initiate total withdrawal requests 
        latestWithdrawalIndex[projectCount] = 0;
        // emit event
        emit NewProjectCreated(
            projectCount,
            msg.sender,
            _name,
            _description,
            _projectDeadline,
            _goal
        );
    }
/** @dev Function to make a contribution to the project
     * @param _id Project ID where contributions are to be made
     */
    function contributeFunds(uint256 _id)
        public
        payable
        checkState(_id, State.Fundraise)
        nonReentrant()
    {
        require(_id <= projectCount, "Project ID out of range");
        require(
            msg.value > 0,
            "Invalid transaction. Please send valid amounts to the project"
        );
        require(
            block.timestamp <= idToProject[_id].projectDeadline,
            "Contributions cannot be made to this project anymore."
        );
        // transfer contributions to contract address
        balances[address(this)] += msg.value;
       // add to contribution
        contributions[_id][msg.sender] += msg.value;
        // increase total contributions pledged to the project
        idToProject[_id].totalPledged += msg.value;
        // add one to total number of depositors for this project
        idToProject[_id].totalDepositors += 1;
        emit FundsReceive(
            _id,
            msg.sender,
            msg.value,
            idToProject[_id].totalPledged
        );
    }
/** @dev Function to end fundraising drive
    * @param _id Project ID
    */
    function endContributionsExpire(uint256 _id) 
        public 
        onlyProjectDonor(_id)
        checkState(_id, State.Fundraise) 
        {
            require(
                block.timestamp > idToProject[_id].projectDeadline,
                "Invalid request. Can only be called after project deadline is reached"
            );
            idToProject[_id].currentState = State.Expire;
            emit ExpireFundraise(_id,
                idToProject[_id].name,
                idToProject[_id].projectDeadline,
                idToProject[_id].goal
            );
        }

    /** @dev Function to end fundraising drive with success is total pledged higher than goal. Irrespective of deadline
    * @param _id Project ID
    */
    function endContributionsSuccess(uint256 _id) 
        public 
        onlyProjectOwner(_id)
        checkState(_id, State.Fundraise) 
        {
            require(idToProject[_id].totalPledged >= idToProject[_id].goal, "Did not receive enough funds");
            idToProject[_id].currentState = State.Success;
            emit SuccessFundRaise(
                _id,
                idToProject[_id].name,
                idToProject[_id].projectDeadline,
                idToProject[_id].goal
            );                
        }
/** @dev Function to get refund on expired projects
     * @param _id Project ID
     */
    function getRefund(uint256 _id)
        public
        payable
        onlyProjectDonor(_id)
        checkState(_id, State.Expire)
        nonReentrant()
    {
        require(
            block.timestamp > idToProject[_id].projectDeadline,
            "Project deadline hasn't been reached yet"
        );
        address payable _contributor = payable(msg.sender);
        uint256 _amount = contributions[_id][msg.sender];
        (bool success, ) = _contributor.call{value: _amount}("");
        require(success, "Transaction failed. Please try again later.");
        emit GenerateRefund(_id, _contributor, _amount);
        // update project state
        idToProject[_id].totalPledged -= _amount;
        idToProject[_id].totalDepositors -= 1;
    }
/** @dev Function to create a request for withdrawal of funds
    * @param _id Project ID
    * @param _requestNumber Index of the request
    * @param _description  Purpose of withdrawal
    * @param _amount Amount of withdrawal requested in ETH
    */
    function createWithdrawalRequest(
        uint256 _id,
        uint32 _requestNumber,
        string memory _description,
        uint256 _amount
    ) public onlyProjectOwner(_id) checkState(_id, State.Success){
        require(idToProject[_id].totalWithdrawn < idToProject[_id].totalPledged, "Insufficient funds");
        require(_requestNumber == latestWithdrawalIndex[_id] + 1, "Incorrect request number");
        // convert ETH to Wei units
        uint256 _withdraw = _amount * 1e18;
        // create new withdrawal request
        WithdrawalRequest memory newWR = WithdrawalRequest({
            index: _requestNumber,
            description: _description,
            withdrawalAmount: _withdraw,
            // funds withdrawn to project owner
            recipient: idToProject[_id].creator,
            // initialized with no votes for request
            approvedVotes: 0,
            // state changes on quorum
            currentWithdrawalState: Withdrawal.Reject,
            withdrawn: false
        });
        // update project to request mapping
        idToWithdrawalRequests[_id].push(newWR);

        latestWithdrawalIndex[_id] += 1;
        // emit event
        emit NewWithdrawalRequest(_id, _description, _amount);
    }
/** @dev Function to check whether a given address has approved a specific request
    * @param _id Project ID
    * @param _withdrawalRequestIndex Index of the withdrawal request
    * @param _checkAddress Address of the request initiator
    */
    function _checkAddressInApprovalsIterator(
        uint256 _id,
        uint32 _withdrawalRequestIndex, 
        address _checkAddress
    )
        internal
        view 
        returns(bool approved) 
    {
        // iterate over the array specific to this id and withdrawal request
        for (uint256 i = 0; i < approvals[_id][_withdrawalRequestIndex - 1].length; i++) {
            // if address is in the array, return true
            if(approvals[_id][_withdrawalRequestIndex - 1][i] == _checkAddress) {
                approved = true;
            }
        }
    }
/** @dev Function to check whether a given address has rejected a specific request
    * @param _id Project ID
    * @param _withdrawalRequestIndex Index of the withdrawal request
    * @param _checkAddress Address of the request initiator
    */
    function _checkAddressInRejectionIterator(
        uint256 _id,
        uint32 _withdrawalRequestIndex, 
        address _checkAddress
    ) 
        internal
        view
        returns(bool rejected) 
    {
        // iterate over the array specific to this id and withdrawal request
        for (uint256 i = 0; i < rejections[_id][_withdrawalRequestIndex - 1].length; i++) {
            // if address is in the array, return true
            if(rejections[_id][_withdrawalRequestIndex - 1][i] == _checkAddress) {
                rejected = true;
            }
        }
    }
/** @dev Function to approve withdrawal of funds
    * @param _id Project ID
    * @param _withdrawalRequestIndex Index of withdrawal request
    */
    function approveWithdrawalRequest(
        uint256 _id,
        uint32 _withdrawalRequestIndex
    )
        public
        onlyProjectDonor(_id)
        checkState(_id, State.Success)
        checkLatestWithdrawalIndex(_id, _withdrawalRequestIndex)
    {
        // confirm msg.sender hasn't approved request yet
        require(!_checkAddressInApprovalsIterator(_id, _withdrawalRequestIndex, msg.sender), 
                "Invalid operation. You have already approved this request");
         require(!_checkAddressInRejectionIterator(_id, _withdrawalRequestIndex, msg.sender), 
                "Invalid operation. You have rejected this request");
        // get total withdrawal requests made
        uint256 _lastWithdrawal = latestWithdrawalIndex[_id];
       // iterate over all requests for this project
        for (uint256 i = 0; i < _lastWithdrawal; i++) {
            // if request number is equal to index
            if(i + 1 == _withdrawalRequestIndex) {
                // increment approval count
                idToWithdrawalRequests[_id][i].approvedVotes += 1;
            }
        }
        // push msg.sender to approvals list for this request
        approvals[_id][_withdrawalRequestIndex - 1].push(msg.sender);

        emit ApproveRequest(_id, _withdrawalRequestIndex);
    }
/** @dev Function to reject withdrawal of funds
     * @param _id Project ID
     * @param _withdrawalRequestIndex Index of withdrawal request
     */
    function rejectWithdrawalRequest(
        uint256 _id,
        uint32 _withdrawalRequestIndex
    )
        public
        onlyProjectDonor(_id)
        checkState(_id, State.Success)
        checkLatestWithdrawalIndex(_id, _withdrawalRequestIndex)
    {
        // confirm user hasn't approved request
        require(!_checkAddressInApprovalsIterator(_id, _withdrawalRequestIndex, msg.sender), 
                "Invalid operation. You have approved this request");
        require(!_checkAddressInRejectionIterator(_id, _withdrawalRequestIndex, msg.sender), 
                "Invalid operation. You have already rejected this request");
        // get total withdrawal requests made
        uint256 _lastWithdrawal = latestWithdrawalIndex[_id];
        // iterate over all requests for this project
        for (uint256 i = 0; i < _lastWithdrawal; i++) {
            // if request number is equal to index
            if(i + 1 == _withdrawalRequestIndex) {
                // if there hve been approvals, decrement
                if(idToWithdrawalRequests[_id][i].approvedVotes != 0) {
                    // decrement approval count
                    idToWithdrawalRequests[_id][i].approvedVotes -= 1;
                } 
                    // else if no one has approved request yet, keep approvals to 0
                else {
                    idToWithdrawalRequests[_id][i].approvedVotes == 0;
                }
            }
        }
        // add msg.sender to rejections list for this request
        rejections[_id][_withdrawalRequestIndex - 1].push(msg.sender);
emit RejectRequest(_id, _withdrawalRequestIndex);
    }
/** @dev Function to transfer funds to project creator
     * @param _id Project ID
     * @param _withdrawalRequestIndex Index of withdrawal request
     */
    function transferWithdrawalRequestFunds(
        uint256 _id,
        uint32 _withdrawalRequestIndex
    )
        public
        payable
        onlyProjectOwner(_id)
        checkLatestWithdrawalIndex(_id, _withdrawalRequestIndex)
        nonReentrant()
    {
        require(
     // _withdrawalRequestIndex - 1 to accomodate 0 start of arrays
            idToWithdrawalRequests[_id][_withdrawalRequestIndex - 1].approvedVotes > (idToProject[_id].totalDepositors).div(2),
            "More than half the total depositors need to approve withdrawal request"
        );
        require(idToWithdrawalRequests[_id][_withdrawalRequestIndex - 1].withdrawn == false, "Withdrawal has laready been made for this request");
         require(idToWithdrawalRequests[_id][_withdrawalRequestIndex - 1].withdrawalAmount < idToProject[_id].totalPledged, "Insufficient funds");
          WithdrawalRequest storage cRequest = idToWithdrawalRequests[_id][_withdrawalRequestIndex - 1];
        // flat 0.3% platform fee
        uint256 platformFee = (cRequest.withdrawalAmount.mul(3)).div(1000);
        (bool pfSuccess, ) = payable(platformAdmin).call{value: platformFee}("");
        require(pfSuccess, "Transaction failed. Please try again later.");
        emit PayPlatform(_id, _withdrawalRequestIndex, platformFee);

        // transfer funds to creator
        address payable _creator = idToProject[_id].creator;
        uint256 _amount = cRequest.withdrawalAmount - platformFee;
        (bool success, ) = _creator.call{value: _amount}("");
        require(success, "Transaction failed. Please try again later.");
        emit PayCreator(_id, _withdrawalRequestIndex, _amount);
        // update states
        cRequest.withdrawn = true;
        idToProject[_id].totalWithdrawn += cRequest.withdrawalAmount;
        emit TransferRequestFunds(_id, _withdrawalRequestIndex);
    }
Enter fullscreen mode Exit fullscreen mode

That would be the base functionality. Next up would be the view functions that allow the frontend to query the blockchain and display states.

/*===== Blockchain get functions =====*/
/** @dev Function to get project details
     * @param _id Project ID
     */
    function getProjectDetails(uint256 _id)
        public
        view
        returns (
            address creator,
            string memory name,
            string memory description,
            uint256 projectDeadline,
            uint256 totalPledged,
            uint256 goal,
            uint256 totalDepositors,
            uint256 totalWithdrawn,
            State currentState
)
    {
        creator = idToProject[_id].creator;
        name = idToProject[_id].name;
        description = idToProject[_id].description;
        projectDeadline = idToProject[_id].projectDeadline;
        totalPledged = idToProject[_id].totalPledged;
        goal = idToProject[_id].goal;
        totalDepositors = idToProject[_id].totalDepositors;
        totalWithdrawn = idToProject[_id].totalWithdrawn;
        currentState = idToProject[_id].currentState;
    }
function getAllProjects() public view returns (Project[] memory) {
        uint256 _projectCount = projectCount;
        Project[] memory projects = new Project[](_projectCount);
        for (uint256 i = 0; i < _projectCount; i++) {
            uint256 currentId = i + 1;
            Project storage currentItem = idToProject[currentId];
            projects[i] = currentItem;
        }
        return projects;
    }
function getProjectCount() public view returns (uint256 count) {
        count = projectCount;
    }
function getAllWithdrawalRequests(uint256 _id)
        public
        view
        returns (WithdrawalRequest[] memory)
    {
        uint256 _lastWithdrawal = latestWithdrawalIndex[_id];
        WithdrawalRequest[] memory withdrawals = new WithdrawalRequest[](
            _lastWithdrawal
        );
        for (uint256 i = 0; i < _lastWithdrawal; i++) {
            WithdrawalRequest storage currentRequest = idToWithdrawalRequests[_id][i];
            withdrawals[i] = currentRequest;
        }
        return withdrawals;
    }
function getContributions(uint256 _id, address _contributor) public view returns(uint256) {
        return contributions[_id][_contributor];
    }
}
Enter fullscreen mode Exit fullscreen mode

Once the contract is written, compile and migrate it onto the testnet with

truffle migrate
Enter fullscreen mode Exit fullscreen mode

In the transaction receipt, copy the contract address and create a file called config.js

export const contractAddress = "0x53692f4BB9E072d481D42Ce2c9d919E2945aDac6";
Enter fullscreen mode Exit fullscreen mode

This address is where I ended up deploying it to the rinkeby testenet.

Yeeeeesh, that was a lot of code.

Go walk a little. kiss your partner. If you don’t have a partner, go talk to someone you’re interested in and when the disappointment of that interaction sinks in… Get started with the frontend.


Initialize Tailwind with NextJS by following the instructions given here.

Don’t be hasty and don’t miss a step. Correct configuration is really important for the whole build to work out in the end.

so I worked with the localhost settings and then commented them out to have the semi-real world settings of testnet activated. But when you’re working on your development environment, reverse the process and poke around a lot more.

I’m coming to love NextJS cuz it pretty much takes away all the unnecessary work away from me and I can focus on building the product.

Before we start, let’s have our package.json files looking the same

{
  "name": "crowdfund",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "resolutions": {
    "async": "^2.6.4",
    "node-fetch": "^2.6.7",
    "lodash": "^4.17.21",
    "underscore": "^1.12.1",
    "yargs-parser": "^5.0.1"
  },
  "dependencies": {
    "@openzeppelin/contracts": "^4.6.0",
    "@openzeppelin/test-helpers": "^0.5.15",
    "@truffle/hdwallet-provider": "^2.0.8",
    "@walletconnect/web3-provider": "^1.7.8",
    "bignumber.js": "^9.0.2",
    "dotenv": "^16.0.1",
    "ipfs-http-client": "^56.0.3",
    "next": "12.1.6",
    "nextjs-progressbar": "^0.0.14",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "true-json-bigint": "^1.0.1",
    "web3": "^1.7.3",
    "web3modal": "^1.9.7"
  },
  "devDependencies": {
    "@babel/plugin-syntax-top-level-await": "^7.14.5",
    "@openzeppelin/truffle-upgrades": "^1.15.0",
    "autoprefixer": "^10.4.7",
    "chai": "^4.3.6",
    "eslint": "8.16.0",
    "eslint-config-next": "12.1.6",
    "ethereum-waffle": "^3.0.0",
    "ethers": "^5.6.8",
    "postcss": "^8.4.14",
    "tailwindcss": "^3.0.24"
  }
}
Enter fullscreen mode Exit fullscreen mode

now run

yarn
Enter fullscreen mode Exit fullscreen mode

And this will install all the dependencies needed for this project.

Next, go into the pages directory and in the _app.js file:

import { AccountContext } from '../context.js'
import { useState } from 'react'
import Link from 'next/link'
import Head from 'next/head'
import NextNProgress from "nextjs-progressbar";
import { ethers } from 'ethers'
import Web3Modal from 'web3modal'
import WalletConnectProvider from '@walletconnect/web3-provider'
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
  const [account, setAccount] = useState(null)
// 1.
   async function getWeb3Modal() {
    const web3Modal = new Web3Modal({
      network: 'rinkeby',
      cacheProvider: false,
      providerOptions: {
        walletconnect: {
          package: WalletConnectProvider,
          // testnet deployement
          options: {
            infuraId: process.env.PROJECT_ID
          },
          // localhost for dev
          // options: {
          //   rpc: { 1337: 'http://localhost:8545', },
          //   chainId: 1337,
          // }
        },
      },
    })
    return web3Modal
  }
//2.
   async function web3connect() {
    try {
      const web3Modal = await getWeb3Modal()
      const connection = await web3Modal.connect()
      const provider = new ethers.providers.Web3Provider(connection)
      const accounts = await provider.listAccounts()
      return accounts;
    } catch (err) {
      console.log('error:', err)
    }
  }
// 3.
   // connect to wallet
  async function connect() {
    const accounts = await web3connect()
    setAccount(accounts)
  }
   return (
    <div className='min-h-screen w-screen font-mono'>
      <Head>
        <title>iFund</title>
        <meta name="description" content="Create New Fundraising Campaign" />
        <link rel="icon" href="/logo.png" />
      </Head>
      <div className='sm:h-10'>
        <nav className='flex mx-auto text-black-20/100'>
          <Link href="/">
            <a>
              <img src="/logo.png" alt="crowdFund logo" className='h-20 object-contain my-5 ml-5' />
            </a>
          </Link>
          <div className='flex'>
// 4.
            {
              !account ?
                <div className='my-10 mx-10' >
                  <p>Pls connect to interact with this app</p>
                  <button className='rounded-md bg-pink-500 text-white p-3 ml-20' onClick={connect}>Connect</button>
                </div> :
                <p className='rounded-md my-10 bg-pink-500 text-white p-3 ml-20' >
                  {account[0].substr(0, 10) + "..."}
                </p>
            }
            {/* if you compile right now it will give an error as we haven't created this page yet. this is coming up next. */}
            <Link href="/create">
              <button className='rounded-md my-10 bg-pink-500 text-white p-3 ml-20' >Create New Fundraising Project</button>
            </Link>
          </div>
        </nav>
      </div>
// 5. 
    {/* drip account into the app */}
      <AccountContext.Provider value={account}>
        <NextNProgress />
        {account && <Component {...pageProps} connect={connect} />}
      </AccountContext.Provider>
    </div>)
}
export default MyApp
Enter fullscreen mode Exit fullscreen mode
  1. contacts the blockchain

  2. calls the blockchain from our metamask infura endpoint

  3. updates state of the account currently talking to the blockchain

  4. is pretty straight forward. If no account has been connected yet, say connect. If something gets connected, print out a few of the first characters of the account

  5. AccountContext is a context we add so that the app stays informed of any changes in the user connecting to it

(you can read more about contexts here)

outside the pages directory create a file called context.js and add this to it

import { createContext } from 'react'
export const AccountContext = createContext(null)
Enter fullscreen mode Exit fullscreen mode

Next we create the page for fundraise creation inside the pages directory called create.js

import Link from "next/link"
import { useEffect, useState } from 'react' // new
import { useRouter } from 'next/router'
import Web3Modal from 'web3modal'
import { ethers } from 'ethers'
// 1.
import CrowdFund from "../build/contracts/CrowdFund.json"
import { contractAddress } from '../config'
// 2.
const initialState = { name: '', description: "'', projectDeadline: '', goal: 0 };"
const Create = () => {
    // router to route back to home page
    const router = useRouter()
    const [project, setProject] = useState(initialState)
    const [contract, setContract] = useState();

// 3.
    useEffect(() => {
        // function to get contract address and update state
        async function getContract() {
            const web3Modal = new Web3Modal()
            const connection = await web3Modal.connect()
            const provider = new ethers.providers.Web3Provider(connection)
            const signer = provider.getSigner()
            let _contract = new ethers.Contract(contractAddress, CrowdFund.abi, signer)
            setContract(_contract);
        }
        getContract();
    })

// 4.
     async function saveProject() {
        // destructure project 
        const { name, description, projectDeadline, goal } = project
        try {
            // create project
            let transaction = await contract.createNewProject(name, description, projectDeadline, goal)
// await successful transaction and reroute to home
            const x = await transaction.wait()
            if (x.status == 1) {
                router.push('/')
            }
        } catch (err) {
            window.alert(err)
        }
    }
return (
        <div className="min-h-screen my-20 w-screen p-5">
            <main>
                <div className="rounded-md my-10 bg-pink-500 text-white p-3 w-20"><Link href="/"> Home </Link></div>
                <p className="text-center text-lg my-5">Create a new campaign!</p>

// 5.
               <div className="bg-pink-500 text-black h-50 p-10 flex flex-col">
                    <input
                        onChange={e => setProject({ ...project, name: e.target.value })}
                        name='title'
                        placeholder='Give it a name ...'
                        className='p-2 my-2 rounded-md'
                        value={project.name}
                    />
                    <textarea
                        onChange={e => setProject({ ...project, description: "e.target.value })}"
                        name='description'
                        placeholder='Give it a description ...'
                        className='p-2 my-2 rounded-md'
                    />
                    <input
                        onChange={e => setProject({ ...project, projectDeadline: Math.floor(new Date() / 1000) + (e.target.value * 86400) })}
                        name='projectDeadline'
                        placeholder='Give it a deadline ... (in days)'
                        className='p-2 my-2 rounded-md'
                    />
                    <input
                        onChange={e => setProject({ ...project, goal: e.target.value })}
                        name='goalEth'
                        placeholder='Give it a goal ... (in ETH). Only integer values are valid'
                        className='p-2 my-2 rounded-md'
                    />
                    <button type='button' className="w-20 text-white rounded-md my-10 px-3 py-2 shadow-lg border-2" onClick={saveProject}>Submit</button>
                </div>
            </main>
        </div>
    )
}
export default Create
Enter fullscreen mode Exit fullscreen mode
  1. is where the contract details are fetched for the frontend. We need 3 things to talk to this contract. The blockchain it’s on, the address of the contract, it’s contents and who’s trying to interact with it. The blockchain and who’s talking to it, we got in _app.js. contract address and its contents (ABI) we fetch from the build that truffle migrate gave us.

  2. I set the initial state as an object of what entries I wanted the fundraise to have. This makes it easy to update state through inputs later on.

  3. since NextJS makes everythig into HTML on the server side if we don’t mention this piece of code in useEffect, we get an error, because of course only when the provider and signer have been set can the contract be contacted.

  4. this simply calls the createNewProject function that we defined in the contract

  5. now we get user input and update state and send this onto the blockchain, hence creating a new project

If you run

yarn run dev
Enter fullscreen mode Exit fullscreen mode

now, this will compile and we’ll have a home page and a create project page.

Now that we can create projects, we need to view all the projects that the creators whip up. So in the home page (index.js) we write:

import {
    contractAddress
} from '../config'
import CrowdFund from "../build/contracts/CrowdFund.json"
import { ethers, BigNumber } from 'ethers'
import Link from 'next/link'
// 1.
const infuraKey = process.env.PROJECT_ID;
export default function Home({ projects }) {
    return (
        <div className='min-h-screen my-20 w-screen p-5'>
            <p className='text-center font-bold'>test project --- Please connect to the Rinkeby testnet</p>
<div className='bg-pink-500 text-white p-10 rounded-md'>
                <div>
                    <p className='font-bold m-5'>How it Works</p>
                    <p className='my-3'>1. Creator creates a new project </p>
<p className='my-3'>2. Contributors contribute until deadline</p>
                    <p className='my-3'>3. If total pledged doesn&apos;t get met on deadline date, contributors expire the project and refund donated funds back</p>
                    <p className='my-3'>4. If total amount pledged reaches the goal, creator declares the fundraise a success</p>
                    <div className='my-3'>
                        <p className='my-3 ml-10'>a. creator makes a withdrawal request</p>
                        <p className='my-3 ml-10'>b. contributors vote on the request</p>
                        <p className='my-3 ml-10'>c. if approved, creator withdraws the amount requested to work on the project</p>
                    </div>
                </div>
            </div>
            <div className='text-black'>
// 2.
                <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pt-6">
                    {
                        projects.map((project, i) => (
                            <div key={i} className="border shadow rounded-xl overflow-hidden">
                                <div className="p-4">
                                    <p>ID: {BigNumber.from(project[0]).toNumber()}</p>
                                    <p className="my-6 text-2xl font-semibold">{project[2]}</p>
                                    <div>
                                        <p className="my-3 text-gray-400">{project[3]?.substr(0, 20) + "..."}</p>
                                        <p className="my-3"> Deadline:  {new Date((BigNumber.from(project[4]).toNumber()) * 1000).toLocaleDateString()} </p>
                                        <p className="my-3"> Total Pledged:  {Math.round(ethers.utils.formatEther(project[5]))} ETH</p>
                                        <p className="my-3"> Goal:  {ethers.utils.formatEther(project[6])} ETH </p>
                                    </div>
// 3. 
<Link href={`project/${BigNumber.from(project[0]).toNumber()}`} key={i}>
                                        <button className='rounded-md my-5 bg-pink-500 text-white p-3 mx-1'>Details</button>
                                    </Link>
                                </div>
                            </div>
                        ))
                    }
                </div>
            </div>
        </div >
    )
}
// 4.
export async function getServerSideProps() {
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
    // localhost
    // let provider = new ethers.providers.JsonRpcProvider()
const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getAllProjects()
    return {
        props: {
            projects: JSON.parse(JSON.stringify(data))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. gets the infura key from the .env file

  2. props that we get from the server-side render gets destructured and come as projects. We map over this and can isolate indiviual project that have been created by users on this contract.

  3. this is the page we create next. the page will error out for now

  4. is where NextJS shines. It fetches all the projects on the server side and ‘hydrates’ (populates) it pre-render. This makes the whole render super fast.

I swear to god, I wish I could make this codeblock a little wider for yall, but alas, such is the formatting. You must contend with it or check out the code on github.

I like dynamic routing a lot so we will use that. in the pages directory, create a directory named project. inside that create a file named [id].js

what this essentially allows us to do is run functions getStaticPaths and getStaticProps which in effect populates all the possible routes that we’ve decided to hook. In this example we hook the project ID with the route.

import { BigNumber, ethers, web3 } from 'ethers'
import Web3Modal from 'web3modal'
import { useState, useContext, useEffect } from 'react' // new
import { useRouter } from 'next/router'
import Link from 'next/link'
import {
    contractAddress
} from '../../config'
import { AccountContext } from '../../context'
import CrowdFund from "../../build/contracts/CrowdFund.json"
const infuraKey = process.env.PROJECT_ID;
export default function Project({ project, projectID }) {
    useContext(AccountContext);
    const router = useRouter()
    const [contributionValue, setContributionValue] = useState(0);
    const [contract, setContract] = useState();
    useEffect(() => {
        // function to get contract address and update state
        async function getContract() {
            const web3Modal = new Web3Modal()
            const connection = await web3Modal.connect()
            const provider = new ethers.providers.Web3Provider(connection)
            const signer = provider.getSigner()
            let _contract = new ethers.Contract(contractAddress, CrowdFund.abi, signer)
            setContract(_contract);
        }
        getContract();
    })
    // Function to contribute funds to the project
    async function contribute() {
        try {
            // send contribution
            let transaction = await contract.contributeFunds(projectID, {
                value: ethers.utils.parseUnits(contributionValue, "ether")
            })
            // await transaction
            let x = await transaction.wait()
            // reroute to home page
            if (x.status == 1) {
                router.push('/')
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
    // function to declare fundraise a success
    async function changeStateToSuccess() {
        try {
            let tx = await contract.endContributionsSuccess(projectID);
            let x = await tx.wait()
            if (x.status == 1) {
                router.push(`/project/${projectID}`);
                window.alert('Project state was successfully changed to : Success')
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
    // function to declare fundraise a failure
    async function changeStateToExpire() {
        try {
            let tx = await contract.endContributionsExpire(projectID);
            let x = await tx.wait()
            if (x.status == 1) {
                window.alert('Project state was successfully changed to : Expire')
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
// function to process a refund on failed fundraise
    async function processRefund() {
        try {
            let tx = await contract.getRefund(projectID);
            let x = await tx.wait()
            if (x.status == 1) {
                window.alert('Successful Refund')
                router.push('/');
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
// 1.
    if (router.isFallback) {
        return <div>Loading...</div>
    }
return (
        <div className='mt-20'>
            <div className='bg-pink-500 text-white p-20 rounded-md mx-5 mt-40'>
                <p className='my-6'><span className='font-bold'> Project Number: </span> {projectID}</p>
                <p className='my-6'><span className='font-bold'> Creator: </span> {project.creator}</p>
                <p className='my-6'><span className='font-bold'> Project Name: </span> {project.name}</p>
                <div className='break-words'>
                    <p className='my-6'><span className='font-bold'>Description:</span> {project.description}</p>
                </div>
                <p className='my-6'><span className='font-bold'>Crowdfund deadline:</span> {new Date((BigNumber.from(project.projectDeadline).toNumber()) * 1000).toLocaleDateString()}</p>
                <p className='my-6'><span className='font-bold'>Total ETH pledged:</span> {project.totalPledged} ETH</p>
                <p className='my-6'><span className='font-bold'>Fundraise Goal:</span> {project.goal} ETH</p>
                <p className='my-6'><span className='font-bold'>Total Contributors:</span> {project.totalDepositors}</p>
                <p className='my-6'><span className='font-bold'>Current State:</span> {project.currentState === 0 ? 'Fundraise Active' : (project.currentState === 1) ? 'Fundraise Expired' : 'Fundraise Success'}</p>
<p className='my-6'><span className='font-bold'>Total Withdrawals:</span> {project.totalWithdrawn} ETH</p>
<div className='text-center'>
                    <input
                        onChange={e => setContributionValue(e.target.value)}
                        type='number'
                        className='p-2 my-2 rounded-md text-black'
                        value={contributionValue}
                    />
                    <button onClick={contribute} className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 mx-4 shadow-md'>Contribute</button>
                </div>
<div className='grid sm:grid-col-1 md:grid-cols-2 sm:text-sm'>
                    <div className='grid grid-cols-1 px-10 sm:w-200 place-content-stretch'>
                        <button onClick={changeStateToSuccess} className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 shadow-lg flex-wrap'>Click here if fundraise was a success (project owner only)</button>
<Link href={`withdrawal/${projectID}`}>
                            <button className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 shadow-lg min-w-50'>Create Withdrawal Request</button>
                        </Link>
<Link href={`requests/${projectID}`}>
                            <button className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 shadow-lg flex-wrap'>Approve / Reject / Withdraw</button>
                        </Link>
                    </div>
<div className='grid grid-cols-1 px-10'>
                        <button onClick={changeStateToExpire} className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 shadow-lg w-50'>Click here if fundraise needs to be expired (contributors only)</button>
<button onClick={processRefund} className='rounded-md mt-20 my-10 bg-white text-pink-500 p-3 shadow-lg w-50'>Request Refund</button>
                    </div>
                </div>
            </div>
        </div >
    )
}
// 2.
export async function getStaticPaths() {
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
// let provider = new ethers.providers.JsonRpcProvider()
    const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getAllProjects()
// populate the dynamic routes with the id
    const paths = data.map(d => ({ params: { id: BigNumber.from(d[0]).toString() } }))
return {
        paths,
        fallback: true
    }
}
// 3.
// local fetch - change to ropsten/mainnet on deployement time
export async function getStaticProps({ params }) {
    // isolate ID from params
    const { id } = params
    // contact the blockchain
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
     // localhost
    // let provider = new ethers.providers.JsonRpcProvider()
    const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getProjectDetails(id);
    // parse received data into JSON
    let projectData = {
        creator: data.creator,
        name: data.name,
        description: data.description,
        projectDeadline: BigNumber.from(data.projectDeadline).toNumber(),
        totalPledged: ethers.utils.formatEther(data.totalPledged),
        goal: ethers.utils.formatEther(data.goal),
        totalDepositors: BigNumber.from(data.totalDepositors).toNumber(),
        totalWithdrawn: ethers.utils.formatEther(data.totalWithdrawn),
        currentState: data.currentState
    }
// return JSON data belonging to this route
    return {
        props: {
            project: projectData,
            projectID: id
        },
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. while routing is happening, we need a fallback otherwise the page has nothing to render and errors out

  2. fetches the project IDs and tells nextJS what the ID route will be hooking on to. in our case it is the project’s ID.

  3. next we pass the params obtained in the last getStaticPaths call to fetch data of the project. We create a JSON object and returns it as props which gets pulled into the main function’s input at the top.

If the project is a success, the creator needs to submit a withdrawal request. For that we create a new folder in the project directory called withdrawal. Inside this we create a file called [id].js and

import { BigNumber, ethers } from 'ethers'
import { useEffect, useState } from 'react'
import Web3Modal from 'web3modal'
import { useRouter } from 'next/router'
import {
    contractAddress
} from '../../../config'
import CrowdFund from "../../../build/contracts/CrowdFund.json"
const infuraKey = process.env.PROJECT_ID;
const initialState = { requestNo: '', requestDescription: '', amount: 0 };
export default function Withdrawal({ projectID }) {
    const router = useRouter()
    const [withdrawalRequest, setWithdrawalRequest] =    useState(initialState)
    const [contract, setContract] = useState();
    useEffect(() => {
        // function to get contract address and update state
        async function getContract() {
            const web3Modal = new Web3Modal()
            const connection = await web3Modal.connect()
            const provider = new ethers.providers.Web3Provider(connection)
            const signer = provider.getSigner()
            let _contract = new ethers.Contract(contractAddress, CrowdFund.abi, signer)
            setContract(_contract);
        }
        getContract();
    })
     async function requestWithdrawal() {
        const { requestNo, requestDescription, amount } = withdrawalRequest;
        try {
            let transaction = await contract.createWithdrawalRequest(projectID, requestNo, requestDescription, amount)
            const x = await transaction.wait()
            if (x.status == 1) {
                router.push(`/project/${projectID}`)
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
    if (router.isFallback) {
        return <div>Loading...</div>
    }
    return (
        <div className='grid sm:grid-cols-1 lg:grid-cols-1 mt-20 '>
            <p className='text-center'>Only project creator can access this functionality on goal reached</p>
            <div className='bg-pink-500 text-black p-20 text-center rounded-md mx-5 flex flex-col'>
                <input
                    type='number'
                    onChange={e => setWithdrawalRequest({ ...withdrawalRequest, requestNo: e.target.value })}
                    name='requestNo'
                    placeholder='Request number...'
                    className='p-2 mt-5 rounded-md'
                    value={withdrawalRequest.requestNo} />
                <textarea
                    onChange={e => setWithdrawalRequest({ ...withdrawalRequest, requestDescription: e.target.value })}
                    name='requestDescription'
                    placeholder='Give it a description ...'
                    className='p-2 mt-5 rounded-md'
                />
                <input
                    onChange={e => setWithdrawalRequest({ ...withdrawalRequest, amount: e.target.value })}
                    name='amount'
                    placeholder='Withdrawal amount ... (in ETH). Only integer values are valid'
                    className='p-2 mt-5 rounded-md'
                />
                <button type='button' className="w-20 bg-white rounded-md my-10 px-3 py-2 shadow-lg border-2" onClick={requestWithdrawal}>Submit</button>
            </div>
        </div >
    )
}
export async function getStaticPaths() {
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
// localhost 
    // let provider = new ethers.providers.JsonRpcProvider()
    const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getAllProjects()
// populate the dynamic routes with the id
    const paths = data.map(d => ({ params: { id: BigNumber.from(d[0]).toString() } }))
return {
        paths,
        fallback: true
    }
}
// local fetch - change to ropsten/mainnet on deployement time
export async function getStaticProps({ params }) {
    // isolate ID from params
    const { id } = params
// return JSON data belonging to this route
    return {
        props: {
            projectID: id
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

the withdrawal requests that have been created need to separated out and individually approved or rejected and the creator needs to be able to withdraw the sum if request gets approved.

In the project directory, create a new directory called requests. Create a new file called [id].js and

import { ethers, BigNumber } from 'ethers'
import Web3Modal from 'web3modal'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import {
    contractAddress
} from '../../../config'
import CrowdFund from "../../../build/contracts/CrowdFund.json"
const infuraKey = process.env.PROJECT_ID;
export default function Requests({ project, projectID }) {
    const router = useRouter()
    const [withdrawalRequests, setWithdrawalRequests] = useState([])
    const [contract, setContract] = useState();
    useEffect(() => {
        // function to get contract address and update state
        async function getContract() {
            const web3Modal = new Web3Modal()
            const connection = await web3Modal.connect()
            const provider = new ethers.providers.Web3Provider(connection)
            const signer = provider.getSigner()
            let _contract = new ethers.Contract(contractAddress, CrowdFund.abi, signer)
            setContract(_contract);
        }
        getContract();
    })

    // function to get all requests made by the creator
    async function getRequests() {
        try {
            let x = await contract.getAllWithdrawalRequests(projectID)
            setWithdrawalRequests(x);
        } catch (err) {
            window.alert(err.message)
        }
    }

    // function to approve a specific request
    async function approveRequest(r, projectID) {
        try {
            let tx = await contract.approveWithdrawalRequest(projectID, r[0])
            let x = await tx.wait()
if (x.status == 1) {
                router.push(`/project/${projectID}`)
            }
        } catch (err) {
            window.alert(err.message)
        }
    }

    // function to reje a specific request
    async function rejectRequest(r, projectID) {
        try {
            let tx = await contract.rejectWithdrawalRequest(projectID, r[0])
            let x = await tx.wait()
if (x.status == 1) {
                router.push(`/project/${projectID}`)
            }
        } catch (err) {
            window.alert(err.message)
        }
    }  
    // function to transfer funds to the creator if requests were approved
    async function transferFunds(r, projectID) {
        try {
            let tx = await contract.transferWithdrawalRequestFunds(projectID, r[0])
            let x = await tx.wait()
if (x.status == 1) {
                router.push(`/project/${projectID}`)
            }
        } catch (err) {
            window.alert(err.message)
        }
    }
    if (router.isFallback) {
        return <div>Loading...</div>
    }
    return (
        <div className='grid sm:grid-cols-1 lg:grid-cols-1 mt-20 '>
            <p className='text-center'>Only project contributors can access approve/reject functionality</p>
            <p className='text-center mt-3 mb-3'>Creators need more than 50% of the contributors to approve a request before withdrawal can be made</p>
<div className='bg-pink-500 text-white p-20 text-center rounded-md'>
                <button onClick={getRequests} className="bg-white text-black rounded-md my-10 px-3 py-2 shadow-lg border-2 w-80">Get all withdrawal requests</button>
<p className='font-bold'>All Withdrawal requests</p>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-6'>
                    {
                        withdrawalRequests.map(request =>
                            <div className='border shadow rounded-xl text-left grid grid-cols-1 lg:grid-col-2' key={request[0]}>
                                <div className='p-4'>
                                    <p className='py-4'>request number: {request[0]}</p>
                                    <p className='py-4 break-words'>description: {request[1]}</p>
                                    <p className='py-4'>amount: {ethers.utils.formatEther(request[2])} ETH</p>
                                    <p className='py-4'>total approvals: {BigNumber.from(request[4]).toNumber()}</p>
                                    <p className='py-4'>total depositor: {project.totalDepositors}</p>
<div className='sm:grid sm:grid-cols-1 xs:grid xs:grid-cols-1'>
                                        <button onClick={() => approveRequest(request, projectID)} className="bg-white text-black rounded-md my-10 mx-1 px-3 py-2 shadow-lg border-2">Approve</button>
<button onClick={() => rejectRequest(request, projectID)} className="bg-white text-black rounded-md my-10 px-3 mx-1 py-2 shadow-lg border-2">Reject</button>
<button onClick={() => transferFunds(request, projectID)} className="bg-white text-black rounded-md my-10 px-3 mx-1 py-2 shadow-lg border-2">Withdraw</button>
                                    </div>
                                </div>
                            </div>
                        )
                    }
                </div>
            </div>
        </div >
    )
}
export async function getStaticPaths() {
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
// localhost
    // let provider = new ethers.providers.JsonRpcProvider()vvvvv
    const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getAllProjects()
// populate the dynamic routes with the id
    const paths = data.map(d => ({ params: { id: BigNumber.from(d[0]).toString() } }))
return {
        paths,
        fallback: true
    }
}
// local fetch - change to ropsten/mainnet on deployement time
export async function getStaticProps({ params }) {
    // isolate ID from params
    const { id } = params
// contact the blockchain
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
// localhost
    // let provider = new ethers.providers.JsonRpcProvider()
const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getProjectDetails(id);
// parse received data into JSON
    let projectData = {
        creator: data.creator,
        name: data.name,
        description: data.description,
        projectDeadline: BigNumber.from(data.projectDeadline).toNumber(),
        totalPledged: ethers.utils.formatEther(data.totalPledged),
        goal: ethers.utils.formatEther(data.goal),
        totalDepositors: BigNumber.from(data.totalDepositors).toNumber(),
        totalWithdrawn: ethers.utils.formatEther(data.totalWithdrawn),
        currentState: data.currentState
    }
// return JSON data belonging to this route
    return {
        props: {
            project: projectData,
            projectID: id
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly we create a 404 page for routes that do not exist. In the pages directory create a file named 404.js

const notFound = () => {
  return (
    <div className='flex h-screen'>
        <p className='m-auto'>404 Not Found</p>
    </div>
  )
}
export default notFound
Enter fullscreen mode Exit fullscreen mode

this finishes up the code. Run

yarn build
Enter fullscreen mode Exit fullscreen mode

to create an optimized build. Push to github and deploy to vercel, and you have a dapp ready to be interacted with.

I hope you got some use out of this. If you did, please help your boy out at

(ETH) 0xF1128dFF5816f80241D6E7de4f3Cc5B5E4c5A819

or

(BTC) 1E8GcSLrzqFBZuxCPSodBg3P8XUV6GXaRK

until next time 🖖

Top comments (0)