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
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
},
}
}
},
};
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
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:
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);
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;
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"
);
_;
}
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);
}
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];
}
}
Once the contract is written, compile and migrate it onto the testnet with
truffle migrate
In the transaction receipt, copy the contract address and create a file called config.js
export const contractAddress = "0x53692f4BB9E072d481D42Ce2c9d919E2945aDac6";
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"
}
}
now run
yarn
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
contacts the blockchain
calls the blockchain from our metamask infura endpoint
updates state of the account currently talking to the blockchain
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
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)
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
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.
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.
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.
this simply calls the createNewProject function that we defined in the contract
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
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'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))
}
}
}
gets the infura key from the .env file
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.
this is the page we create next. the page will error out for now
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
},
}
}
while routing is happening, we need a fallback otherwise the page has nothing to render and errors out
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.
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
},
}
}
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
},
}
}
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
this finishes up the code. Run
yarn build
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)