Краудфандинговый dApp с Solidity и NextJS

Я довольно начинающий разработчик и обнаружил, что учиться коду проще всего, когда я создаю проект, поэтому я решил создать такой проект.

Я создал сайт крауд-финансирования, который не требует посредников. Конечные пользователи приходят и просто начинают взаимодействовать с платформой. Вы можете проверить это здесь.

Ладно, хватит болтать, давайте приступим к делу.

Начнем с инициализации 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
Enter fullscreen mode Выйти из полноэкранного режима
  1. обращается к блокчейну

  2. вызывает блокчейн из конечной точки metamask infura

  3. обновляет состояние учетной записи, которая в данный момент общается с блокчейном

  4. это довольно просто. Если ни один аккаунт еще не подключился, скажите connect. Если что-то было подключено, выведите несколько первых символов счета.

  5. 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
Вход в полноэкранный режим Выйти из полноэкранного режима
  1. это место, откуда извлекаются детали контракта для фронтенда. Нам нужно 3 вещи, чтобы общаться с этим контрактом. Блокчейн, на котором он находится, адрес контракта, его содержимое и кто пытается с ним взаимодействовать. Блокчейн и кто с ним общается, мы получили в _app.js. Адрес контракта и его содержимое (ABI) мы получаем из сборки, которую нам предоставил truffle migrate.

  2. Я задал начальное состояние как объект того, какие записи я хотел, чтобы были у сбора средств. Это облегчает последующее обновление состояния через вводы.

  3. Поскольку NextJS превращает все в HTML на стороне сервера, если мы не упомянем этот кусок кода в useEffect, мы получим ошибку, потому что, конечно, только когда поставщик и подписант установлены, можно связаться с контрактом.

  4. здесь просто вызывается функция createNewProject, которую мы определили в контракте

  5. теперь мы получаем данные от пользователя, обновляем состояние и отправляем их на блокчейн, создавая тем самым новый проект.

Если вы запустите

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&apos;t get met on deadline date, contributors expire the project and refund donated funds back</p>
                    <p className='my-3'>4. If total amount pledged reaches the goal, creator declares the fundraise a success</p>
                    <div className='my-3'>
                        <p className='my-3 ml-10'>a. creator makes a withdrawal request</p>
                        <p className='my-3 ml-10'>b. contributors vote on the request</p>
                        <p className='my-3 ml-10'>c. if approved, creator withdraws the amount requested to work on the project</p>
                    </div>
                </div>
            </div>
            <div className='text-black'>
// 2.
                <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pt-6">
                    {
                        projects.map((project, i) => (
                            <div key={i} className="border shadow rounded-xl overflow-hidden">
                                <div className="p-4">
                                    <p>ID: {BigNumber.from(project[0]).toNumber()}</p>
                                    <p className="my-6 text-2xl font-semibold">{project[2]}</p>
                                    <div>
                                        <p className="my-3 text-gray-400">{project[3]?.substr(0, 20) + "..."}</p>
                                        <p className="my-3"> Deadline:  {new Date((BigNumber.from(project[4]).toNumber()) * 1000).toLocaleDateString()} </p>
                                        <p className="my-3"> Total Pledged:  {Math.round(ethers.utils.formatEther(project[5]))} ETH</p>
                                        <p className="my-3"> Goal:  {ethers.utils.formatEther(project[6])} ETH </p>
                                    </div>
// 3. 
<Link href={`project/${BigNumber.from(project[0]).toNumber()}`} key={i}>
                                        <button className='rounded-md my-5 bg-pink-500 text-white p-3 mx-1'>Details</button>
                                    </Link>
                                </div>
                            </div>
                        ))
                    }
                </div>
            </div>
        </div >
    )
}
// 4.
export async function getServerSideProps() {
    let provider = new ethers.providers.JsonRpcProvider(`https://rinkeby.infura.io/v3/${infuraKey}`)
    // localhost
    // let provider = new ethers.providers.JsonRpcProvider()
const contract = new ethers.Contract(contractAddress, CrowdFund.abi, provider)
    const data = await contract.getAllProjects()
    return {
        props: {
            projects: JSON.parse(JSON.stringify(data))
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. получает ключ infura из файла .env

  2. реквизит, который мы получаем от рендера на стороне сервера, деструктурируется и приходит как проекты. Мы создаем карту и можем изолировать отдельные проекты, которые были созданы пользователями по этому контракту.

  3. Это страница, которую мы создадим дальше. Пока что страница будет работать с ошибками.

  4. Вот где 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
        },
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима
  1. пока происходит маршрутизация, нам нужен запасной вариант, иначе странице нечего будет отрисовывать и произойдет ошибка

  2. получает ID проекта и сообщает nextJS, к какому ID будет подключаться маршрут. в нашем случае это ID проекта.

  3. Далее мы передаем параметры, полученные в последнем вызове 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

до следующего раза 🖖

Оцените статью
Procodings.ru
Добавить комментарий