Я довольно начинающий разработчик и обнаружил, что учиться коду проще всего, когда я создаю проект, поэтому я решил создать такой проект.
Я создал сайт крауд-финансирования, который не требует посредников. Конечные пользователи приходят и просто начинают взаимодействовать с платформой. Вы можете проверить это здесь.
Ладно, хватит болтать, давайте приступим к делу.
Начнем с инициализации truffle с моей основной директории:
truffle init
Мне нравится работать с ganache appimage, поэтому запустите его, и он создаст для нас локальный testnet на порту 8545.
Зайдите в мой truffle.config.js и внесите необходимые изменения
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
},
}
}
},
};
Я закончил тем, что разместил проект на rinkeby testnet, поэтому я добавил сюда конечные точки infura.
Перейдите на приборную панель infura, зарегистрируйтесь и настройте конечную точку для вашего проекта. Инструкции находятся здесь. Когда проект настроен, зайдите в настройки, скопируйте ID проекта и вставьте его в файл .env в корневом каталоге.
PROJECT_ID=db0b4735bad24926a761d909e1f82576
Поскольку этот проект находится в открытом доступе, я поместил это сюда. Вы не захотите делать этого для производственного материала. Что-то связанное с нарушением безопасности и прочее. Я не знаю точных деталей, но углубление в безопасность — мой следующий шаг, так что когда-нибудь я напишу пост и буду знать, почему, черт возьми, это небезопасно.
Мнемоника — это приватный ключ от учетной записи, которая в итоге развернет это в тестовой сети, так что убедитесь, что вы получили несколько фальшивых ETH из крана rinkeby перед развертыванием.
Я выложу код здесь, потому что он хорошо документирован и не требует пояснений:
github repo находится здесь.
Обычно мне нравится определять все события в контракте в самом начале, потому что это дает мне ощущение всех вещей, которые будут происходить в этом контракте
// 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);
Далее мы определяем все переменные состояния. Это все те вещи, которые имеют состояние и будут каким-то образом изменяться во время взаимодействия с пользователями.
/*===== 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;
Далее мы вводим некоторые модификаторы, чтобы установить строгие ограничения на то, кто может взаимодействовать с контрактными функциями.
/*===== 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"
);
_;
}
Я добавил защиту от реентерабельности, потому что я просто не хочу, чтобы люди занимались жадностью, когда речь идет об общественном благе.
Защитой для этого контракта является администратор платформы. Если вокруг плавают какие-то эфиры, лучше отправить их администратору, чем они пропадут впустую, сидя в контракте.
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);
}
Это базовая функциональность. Следующими будут функции представления, которые позволят фронтенду запрашивать блокчейн и отображать состояния.
/*===== 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];
}
}
Как только контракт будет написан, скомпилируйте его и перенесите в тестовую сеть с помощью функции
truffle migrate
В квитанции транзакции скопируйте адрес контракта и создайте файл config.js
export const contractAddress = "0x53692f4BB9E072d481D42Ce2c9d919E2945aDac6";
По этому адресу я в итоге развернул его в тестовой сети rinkeby.
Иииииш, это было много кода.
Пойдите погуляйте немного. Поцелуйте своего партнера. Если у вас нет партнера, поговорите с кем-нибудь, кто вам интересен, и когда разочарование от этого общения пройдет… Приступайте к фронтенду.
Инициализируйте Tailwind с помощью NextJS, следуя инструкциям, приведенным здесь.
Не спешите и не пропускайте ни одного шага. Правильная настройка очень важна для того, чтобы вся сборка в итоге сработала.
Поэтому я работал с настройками localhost, а затем закомментировал их, чтобы активировать полуреальные настройки testnet. Но когда вы работаете над своей средой разработки, обратите этот процесс вспять и побольше поковыряйтесь в настройках.
Я полюбил NextJS за то, что он практически снимает с меня всю ненужную работу, и я могу сосредоточиться на создании продукта.
Прежде чем мы начнем, давайте приведем наши файлы package.json к единому виду
{
"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"
}
}
запустить
yarn
Это установит все зависимости, необходимые для этого проекта.
Далее перейдите в каталог pages и в файл _app.js:
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
-
обращается к блокчейну
-
вызывает блокчейн из конечной точки metamask infura
-
обновляет состояние учетной записи, которая в данный момент общается с блокчейном
-
это довольно просто. Если ни один аккаунт еще не подключился, скажите connect. Если что-то было подключено, выведите несколько первых символов счета.
-
AccountContext — это контекст, который мы добавляем для того, чтобы приложение оставалось в курсе любых изменений в пользователе, подключившемся к нему.
(подробнее о контекстах вы можете прочитать здесь)
Вне каталога pages создайте файл context.js и добавьте в него следующее
import { createContext } from 'react'
export const AccountContext = createContext(null)
Далее мы создадим страницу для создания фонда внутри каталога страниц под названием 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
-
это место, откуда извлекаются детали контракта для фронтенда. Нам нужно 3 вещи, чтобы общаться с этим контрактом. Блокчейн, на котором он находится, адрес контракта, его содержимое и кто пытается с ним взаимодействовать. Блокчейн и кто с ним общается, мы получили в _app.js. Адрес контракта и его содержимое (ABI) мы получаем из сборки, которую нам предоставил truffle migrate.
-
Я задал начальное состояние как объект того, какие записи я хотел, чтобы были у сбора средств. Это облегчает последующее обновление состояния через вводы.
-
Поскольку NextJS превращает все в HTML на стороне сервера, если мы не упомянем этот кусок кода в useEffect, мы получим ошибку, потому что, конечно, только когда поставщик и подписант установлены, можно связаться с контрактом.
-
здесь просто вызывается функция createNewProject, которую мы определили в контракте
-
теперь мы получаем данные от пользователя, обновляем состояние и отправляем их на блокчейн, создавая тем самым новый проект.
Если вы запустите
yarn run dev
теперь это скомпилируется, и у нас будет домашняя страница и страница создания проекта.
Теперь, когда мы можем создавать проекты, нам нужно просмотреть все проекты, которые создают создатели. Поэтому на главной странице (index.js) мы напишем:
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))
}
}
}
-
получает ключ infura из файла .env
-
реквизит, который мы получаем от рендера на стороне сервера, деструктурируется и приходит как проекты. Мы создаем карту и можем изолировать отдельные проекты, которые были созданы пользователями по этому контракту.
-
Это страница, которую мы создадим дальше. Пока что страница будет работать с ошибками.
-
Вот где NextJS сияет. Он собирает все проекты на стороне сервера и «гидратирует» (заполняет) их перед рендерингом. Это делает весь рендеринг супербыстрым.
Клянусь богом, я хотел бы сделать этот блок кода немного шире для вас, но увы, таково форматирование. Вам придется смириться с этим или посмотреть код на github.
Мне очень нравится динамическая маршрутизация, поэтому мы будем использовать ее. В каталоге pages создайте каталог с именем project. Внутри него создайте файл [id].js.
Это позволит нам запустить функции getStaticPaths и getStaticProps, которые, по сути, заполнят все возможные маршруты, которые мы решили подключить. В этом примере мы привязываем ID проекта к маршруту.
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
},
}
}
-
пока происходит маршрутизация, нам нужен запасной вариант, иначе странице нечего будет отрисовывать и произойдет ошибка
-
получает ID проекта и сообщает nextJS, к какому ID будет подключаться маршрут. в нашем случае это ID проекта.
-
Далее мы передаем параметры, полученные в последнем вызове getStaticPaths, чтобы получить данные о проекте. Мы создаем JSON-объект и возвращаем его в качестве props, который попадает на вход основной функции в верхней части.
Если проект успешен, создателю необходимо отправить запрос на вывод средств. Для этого мы создаем новую папку в каталоге проекта под названием withdrawal. Внутри нее мы создаем файл [id].js и
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
},
}
}
созданные запросы на вывод средств должны быть разделены и индивидуально одобрены или отклонены, а создатель должен иметь возможность вывести сумму, если запрос будет одобрен.
В каталоге проекта создайте новый каталог requests. Создайте новый файл под названием [id].js и
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
},
}
}
Наконец, мы создадим страницу 404 для несуществующих маршрутов. В директории pages создайте файл с именем 404.js
const notFound = () => {
return (
<div className='flex h-screen'>
<p className='m-auto'>404 Not Found</p>
</div>
)
}
export default notFound
На этом мы закончим работу над кодом. Запустите
yarn build
для создания оптимизированной сборки. Выложите на github и разверните на vercel, и у вас есть dapp, готовый к взаимодействию.
Я надеюсь, что вы получили от этого какую-то пользу. Если это так, пожалуйста, помогите своему мальчику по адресу
(ETH) 0xF1128dFF5816f80241D6E7de4f3Cc5B5E4c5A819
или
(BTC) 1E8GcSLrzqFBZuxCPSodBg3P8XUV6GXaRK
до следующего раза 🖖