DAO开发教程WEB3.0

Posted 新缸中之脑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DAO开发教程WEB3.0相关的知识,希望对你有一定的参考价值。

在这个教程中,我们将学习如何开发一个支持聊天的去中心化自治组织 (DAO),教程内容
涵盖使用的工具链、智能合约开发部署和前端应用开发。

用熟悉的语言学习 Web3.0 开发Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、使用的工具链

我们需要安装以下工具来完成此教程:

  • Node.js
  • Ganache-Cli
  • Truffle
  • React
  • Infura
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

2、安装开发依赖

2.1 NodeJs安装

确保你的机器上已经安装了 NodeJs。接下来,在终端上运行代码以确认它已安装。

2.2 Yarn、Ganache-cli 和 Truffle 安装

在终端上运行以下代码以全局安装这些基本软件包。

npm i -g yarn
npm i -g truffle
npm i -g ganache-cli

2.3 克隆 Web3 入门项目

使用下面的命令,克隆下面的 web 3.0 入门项目。这将确保我们都在同一个页面上并使用相同的包。

git clone https://github.com/Daltonic/dominionDAO

太棒了,让我们用下面的文件替换package.json文件:


  "name": "dominionDAO",
  "private": true,
  "version": "0.0.0",
  "scripts": 
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  ,
  "dependencies": 
    "@cometchat-pro/chat": "3.0.6",
    "moment": "^2.29.3",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hooks-global-state": "^1.0.2",
    "react-icons": "^4.3.1",
    "react-identicons": "^1.2.5",
    "react-moment": "^1.1.2",
    "react-router-dom": "6",
    "react-scripts": "5.0.0",
    "react-toastify": "^9.0.1",
    "recharts": "^2.1.9",
    "web-vitals": "^2.1.4",
    "web3": "^1.7.1"
  ,
  "devDependencies": 
    "@openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "@truffle/hdwallet-provider": "^2.0.4",
    "assert": "^2.0.0",
    "autoprefixer": "10.4.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "babel-register": "^6.26.0",
    "buffer": "^6.0.3",
    "chai": "^4.3.6",
    "chai-as-promised": "^7.1.1",
    "crypto-browserify": "^3.12.0",
    "dotenv": "^16.0.0",
    "https-browserify": "^1.0.0",
    "mnemonics": "^1.1.3",
    "os-browserify": "^0.3.0",
    "postcss": "8.4.5",
    "process": "^0.11.10",
    "react-app-rewired": "^2.1.11",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tailwindcss": "3.0.18",
    "url": "^0.11.0"
  ,
  "browserslist": 
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  

太好了,用上面的代码替换你的package.json文件,然后在你的终端上运行yarn install

安装完毕后,让我们开始编写 Dominion DAO 智能合约。

3、配置 CometChat SDK

要配置CometChat SDK,请按照以下步骤操作,最后,我们需要将这些密钥存储为环境变量。

第 1 步: 前往CometChat仪表板并创建一个帐户。

第 2步:在注册后登录 CometChat 仪表 板。

第 3 步: 在仪表板中,添加一个名为dominionDAO 的新应用程序。

第 4 步: 从列表中选择刚刚创建的应用程序。

第 5 步: 从快速入门中将APP_ID、REGION和AUTH_KEY, 复制到你的.env文件中。请参阅图像和代码片段。

REACT_COMET_CHAT占位符键替换为相应的值:

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

4、配置 Infura

第 1 步: 前往Infura创建一个帐户。

第 2 步: 从Infura仪表板创建一个新项目。

第 3 步: 将Rinkeby测试网络 WebSocket 端点 URL 复制到你的.env文件中。

接下来,添加你的 Metamask 密码短语和首选帐户私钥。如果正确地完成了这些操作,你的环境变量现在应该如下所示。

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

如果不知道如何访问你的私钥,请参阅下面的部分。

5、访问 Metamask 私钥

第 1 步: 单击Metamask浏览器扩展程序,并确保Rinkeby已选择作为测试网络。

接下来,在首选帐户上,单击垂直虚线并选择帐户详细信息。见下图。

第 2 步: 在提供的字段中输入你的密码,然后单击确认按钮,这将使你能够访问你的帐户私钥。

第 3 步: 单击“导出私钥”以查看你的私钥。确保永远不会在公共页面上公开你的密钥,例如Github. 这就是为什么我们将其附加为环境变量。

第 4 步: 将你的私钥复制到 .env 文件中。请参阅下面的图像和代码片段:

ENDPOINT_URL=***************************
SECRET_KEY=******************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

至于SECRET_KEY,你需要将你的Metamask密码短语粘贴到环境文件中提供的空间中。

6、Dominion DAO 智能合约

这是智能合约的完整代码,我将逐个解释所有函数和变量。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DominionDAO is ReentrancyGuard, AccessControl 
    bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
    bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
    uint32 immutable MIN_VOTE_DURATION = 1 weeks;
    uint256 totalProposals;
    uint256 public daoBalance;

    mapping(uint256 => ProposalStruct) private raisedProposals;
    mapping(address => uint256[]) private stakeholderVotes;
    mapping(uint256 => VotedStruct[]) private votedOn;
    mapping(address => uint256) private contributors;
    mapping(address => uint256) private stakeholders;

    struct ProposalStruct 
        uint256 id;
        uint256 amount;
        uint256 duration;
        uint256 upvotes;
        uint256 downvotes;
        string title;
        string description;
        bool passed;
        bool paid;
        address payable beneficiary;
        address proposer;
        address executor;
    

    struct VotedStruct 
        address voter;
        uint256 timestamp;
        bool choosen;
    

    event Action(
        address indexed initiator,
        bytes32 role,
        string message,
        address indexed beneficiary,
        uint256 amount
    );

    modifier stakeholderOnly(string memory message) 
        require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
        _;
    

    modifier contributorOnly(string memory message) 
        require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
        _;
    

    function createProposal(
        string calldata title,
        string calldata description,
        address beneficiary,
        uint256 amount
    )external
     stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
    
        uint256 proposalId = totalProposals++;
        ProposalStruct storage proposal = raisedProposals[proposalId];

        proposal.id = proposalId;
        proposal.proposer = payable(msg.sender);
        proposal.title = title;
        proposal.description = description;
        proposal.beneficiary = payable(beneficiary);
        proposal.amount = amount;
        proposal.duration = block.timestamp + MIN_VOTE_DURATION;

        emit Action(
            msg.sender,
            CONTRIBUTOR_ROLE,
            "PROPOSAL RAISED",
            beneficiary,
            amount
        );
    

    function performVote(uint256 proposalId, bool choosen)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
    
        ProposalStruct storage proposal = raisedProposals[proposalId];

        handleVoting(proposal);

        if (choosen) proposal.upvotes++;
        else proposal.downvotes++;

        stakeholderVotes[msg.sender].push(proposal.id);

        votedOn[proposal.id].push(
            VotedStruct(
                msg.sender,
                block.timestamp,
                choosen
            )
        );

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PROPOSAL VOTE",
            proposal.beneficiary,
            proposal.amount
        );
    

    function handleVoting(ProposalStruct storage proposal) private 
        if (
            proposal.passed ||
            proposal.duration <= block.timestamp
        ) 
            proposal.passed = true;
            revert("Proposal duration expired");
        

        uint256[] memory tempVotes = stakeholderVotes[msg.sender];
        for (uint256 votes = 0; votes < tempVotes.length; votes++) 
            if (proposal.id == tempVotes[votes])
                revert("Double voting not allowed");
        
    

    function payBeneficiary(uint256 proposalId)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
        returns (bool)
    
        ProposalStruct storage proposal = raisedProposals[proposalId];
        require(daoBalance >= proposal.amount, "Insufficient fund");
        require(block.timestamp > proposal.duration, "Proposal still ongoing");

        if (proposal.paid) revert("Payment sent before");

        if (proposal.upvotes <= proposal.downvotes)
            revert("Insufficient votes");

        payTo(proposal.beneficiary, proposal.amount);

        proposal.paid = true;
        proposal.executor = msg.sender;
        daoBalance -= proposal.amount;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PAYMENT TRANSFERED",
            proposal.beneficiary,
            proposal.amount
        );

        return true;
    

    function contribute() payable external 

        if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) 
            uint256 totalContribution =
                contributors[msg.sender] + msg.value;

            if (totalContribution >= 5 ether) 
                stakeholders[msg.sender] = totalContribution;
                contributors[msg.sender] += msg.value;
                _setupRole(STAKEHOLDER_ROLE, msg.sender);
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
             else 
                contributors[msg.sender] += msg.value;
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
            
         else 
            contributors[msg.sender] += msg.value;
            stakeholders[msg.sender] += msg.value;
        
        
        daoBalance += msg.value;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "CONTRIBUTION RECEIVED",
            address(this),
            msg.value
        );
    

    function getProposals()
        external
        view
        returns (ProposalStruct[] memory props)
    
        props = new ProposalStruct[](totalProposals);

        for (uint256 i = 0; i < totalProposals; i++) 
            props[i] = raisedProposals[i];
        
    

    function getProposal(uint256 proposalId)
        external
        view
        returns (ProposalStruct memory)
    
        return raisedProposals[proposalId];
    
    
    function getVotesOf(uint256 proposalId)
        external
        view
        returns (VotedStruct[] memory)
    
        return votedOn[proposalId];
    

    function getStakeholderVotes()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256[] memory)
    
        return stakeholderVotes[msg.sender];
    

    function getStakeholderBalance()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256)
    
        return stakeholders[msg.sender];
    

    function isStakeholder() external view returns (bool) 
        return stakeholders[msg.sender] > 0;
    

    function getContributorBalance()
        external
        view
        contributorOnly("Denied: User is not a contributor")
        returns (uint256)
    
        return contributors[msg.sender];
    

    function isContributor() external view returns (bool) 
        return contributors[msg.sender] > 0;
    

    function getBalance() external view returns (uint256) 
        return contributors[msg.sender];
    

    function payTo(
        address to, 
        uint256 amount
    ) internal returns (bool) 
        (bool success,) = payable(to).callvalue: amount("");
        require(success, "Payment failed");
        return true;
    

在刚刚克隆的项目中,前往src >> contract目录并创建一个名为 DominionDAO.sol的文件,然后将上述
代码粘贴到其中。

6.1 pragma语句

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

Solidity 需要一个许可证标识符来编译你的代码,否则它会产生一个警告,要求你指定一个。此外,Solidity 要求你为智能合约指定编译器的版本。这就是pragma这个词所代表的。

6.2 import语句

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

在上面的代码块中,我们使用import导入两个openzeppelin’s智能合约来指定角色并保护我们的智能合约免受重入攻击。

6.3 DAO角色相关的状态变量

bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;

我们为利益相关者和贡献者角色设置了一些状态变量,并将最短投票持续时间指定为一周。我们还初始化了总提案计数器和一个变量来记录我们的可用余额。

6.4 DAO提案和投票相关的状态变量

mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;

raisedProposals跟踪提交给我们智能合约的所有提案。stakeholderVotes顾名思义,跟踪利益相关者的投票。votedOn跟踪与
提案相关的所有投票。贡献者跟踪向我们平台捐款的任何人,而1 ether以上的贡献者被视为利益相关者进行跟踪。

6.5 DAO提案和投票的数据结构

struct ProposalStruct 
    uint256 id;
    uint256 amount;
    uint256 duration;
    uint256 upvotes;
    uint256 downvotes;
    string title;
    string description;
    bool passed;
    bool paid;
    address payable beneficiary;
    address proposer;
    address executor;


struct VotedStruct 
    address voter;
    uint256 timestamp;
    bool choosen;

proposalStruct描述每个提案的内容,而votedStruct描述每个投票的内容。

6.6 Action事件

event Action(
    address indexed initiator,
    bytes32 role,
    string message,
    address indexed beneficiary,
    uint256 amount
);

这是一个名为 Action 的动态事件。这将帮助我们丰富每笔交易注销的信息。

6.7 DAO角色相关的修饰符

modifier stakeholderOnly(string memory message) 
    require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
    _;


modifier contributorOnly(string memory message) 
    require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
    _;

上述修饰符帮助我们按角色识别用户,也可以防止他们访问一些未经授权的资源。

6.8 DAO提案创建方法

function createProposal(
    string calldata title,
    string calldata description,
    address beneficiary,
    uint256 amount
)external
 stakeholderOnly("Proposal Creation Allowed for Stakeholders only")

    uint256 proposalId = totalProposals++;
    ProposalStruct storage proposal = raisedProposals[proposalId];

    proposal.id = proposalId;
    proposal.proposer = payable(msg.sender);
    proposal.title = title;
    proposal.description = description;
    proposal.beneficiary = payable(beneficiary);
    proposal.amount = amount;
    proposal.duration = block.timestamp + MIN_VOTE_DURATION;

    emit Action(
        msg.sender,
        CONTRIBUTOR_ROLE,
        "PROPOSAL RAISED",
        beneficiary,
        amount
    );

上述函数获取提案的标题、描述、金额和受益人的钱包地址并创建提案。该功能仅允许利益相关者创建提案。利益相关者是至少做出了1 ether贡献的用户。

6.9 DAO投票方法

function performVote(uint256 proposalId, bool choosen)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")

    ProposalStruct storage proposal = raisedProposals[proposalId];

    handleVoting(proposal);

    if (choosen) proposal.upvotes++;
    else proposal.downvotes++;

    stakeholderVotes[msg.sender].push(proposal.id);

    votedOn[proposal.id].push(
        VotedStruct(
            msg.sender,
            block.timestamp,
            choosen
        )
    );

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PROPOSAL VOTE",
        proposal.beneficiary,
        proposal.amount
    );

此函数接受两个参数,一个提案 ID 和一个由布尔值表示的首选选项。True 表示接受投票,False 表示拒绝。

6.10 DAO投票执行方法

function handleVoting(ProposalStruct storage proposal) private 
    if (
        proposal.passed ||
        proposal.duration <= block.timestamp
    ) 
        proposal.passed = true;
        revert("Proposal duration expired");
    

    uint256[] memory tempVotes = stakeholderVotes[msg.sender];
    for (uint256 votes = 0; votes < tempVotes.length; votes++) 
        if (proposal.id == tempVotes[votes])
            revert("Double voting not allowed");
    

此函数执行实际投票,包括检查用户是否是利益相关者并有资格投票。

6.11 DAO受益人支付方法

function payBeneficiary(uint256 proposalId)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")
    returns (bool)

    ProposalStruct storage proposal = raisedProposals[proposalId];
    require(daoBalance >= proposal.amount, "Insufficient fund");
    require(block.timestamp > proposal.duration, "Proposal still ongoing");

    if (proposal.paid) revert("Payment sent before");

    if (proposal.upvotes <= proposal.downvotes)
        revert("Insufficient votes");

    payTo(proposal.beneficiary, proposal.amount);

    proposal.paid = true;
    proposal.executor = msg.sender;
    daoBalance -= proposal.amount;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PAYMENT TRANSFERED",
        proposal.beneficiary,
        proposal.amount
    );

    return true;


此功能负责根据特定标准向提案所附的受益人付款:

  • 受益人不得已经支付。
  • 案期限必须已过期。
  • 可用余额必须能够支付给受益人。
  • 票数不得平分。

6.12 DAO捐款方法

function contribute() payable external 
    if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) 
        uint256 totalContribution =
            contributors[msg.sender] + msg.value;

        if (totalContribution >= 5 ether) 
            stakeholders[msg.sender] = totalContribution;
            contributors[msg.sender] += msg.value;
            _setupRole(STAKEHOLDER_ROLE, msg.sender);
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
         else 
            contributors[msg.sender] += msg.value;
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
        
     else 
        contributors[msg.sender] += msg.value;
        stakeholders[msg.sender] += msg.value;
    
    
    daoBalance += msg.value;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "CONTRIBUTION RECEIVED",
        address(this),
        msg.value
    );

该函数负责从捐助者和有兴趣成为利益相关者的人那里收集捐款。

6.13 DAO提案查询方法

function getProposals()
    external
    view
    returns (ProposalStruct[] memory props)

    props = new ProposalStruct[](totalProposals);

    for (uint256 i = 0; i < totalProposals; i++) 
        props[i] = raisedProposals[i];
    

上面函数检索记录在此智能合约上的一组提案。

6.14 DAO提案详情读取方法

function getProposal(uint256 proposalId)
    external
    view
    returns (ProposalStruct memory)

    return raisedProposals[proposalId];

上面函数按 Id 检索特定提案。

6.15 DAO投票查询方法

function getVotesOf(uint256 proposalId)
    external
    view
    returns (VotedStruct[] memory)

    return votedOn[proposalId];

这将返回与特定提案相关的投票列表。

6.16 DAO利益相关者投票查询方法

function getStakeholderVotes()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256[] memory)

    return stakeholderVotes[msg.sender];

这将返回智能合约上的利益相关者列表,并且只有利益相关者才能调用此函数。

6.17 DAO利益相关者余额查询方法

function getStakeholderBalance()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256)

    return stakeholders[msg.sender];

这将返回利益相关者贡献的金额。

6.18 DAO利益相关者判别方法

function isStakeholder() external view returns (bool) 
    return stakeholders[msg.sender] > 0;

判断用户是否为利益相关者,返回 True 或 False。

6.19 DAO一般贡献者余额查询方法

function getContributorBalance()
    external
    view
    contributorOnly("Denied: User is not a contributor")
    returns (uint256)

    return contributors[msg.sender];

这将返回贡献者的余额,并且只有贡献者可以访问。

6.20 DAO一般贡献者判别方法

function isContributor() external view returns (bool) 
    return contributors[msg.sender] > 0;

这会检查用户是否是贡献者,并用 True 或 False 表示。

6.21 DAO普通用户余额查询方法

function getBalance() external view returns (uint256) 
    return contributors[msg.sender];

返回调用用户的余额,无论其角色如何。

6.22 DAO支付方法

function payTo(
    address to, 
    uint256 amount
) internal returns (bool) 
    (bool success,) = payable(to).callvalue: amount("");
    require(success, "Payment failed");
    return true;

此函数执行指定金额和帐户的付款。

7、配置DAO合约部署脚本

与智能合约有关的另一件事是配置部署脚本。

前往项目的迁移文件夹中的 2_deploy_contracts.js 文件,并使用下面的代码片段对其进行更新。

const DominionDAO = artifacts.require('DominionDAO')
  module.exports = async function (deployer) 
  await deployer.deploy(DominionDAO)

太棒了,我们刚刚完成了应用程序的智能合约,是时候开始构建 Dapp 界面了。

8、开发DAO应用前端

前端包括许多组件和部件。我们将创建所有组件、视图和其余外围设备。

8.1 DAO应用标题栏组件

该组件捕获有关当前用户的信息,并带有一个用于明暗模式的主题切换按钮。这是通过 Tailwind CSS 实现的,具体请参阅下面的代码。

import  useState, useEffect  from 'react'
import  FaUserSecret  from 'react-icons/fa'
import  MdLightMode  from 'react-icons/md'
import  FaMoon  from 'react-icons/fa'
import  Link  from 'react-router-dom'
import  connectWallet  from '../Dominion'
import  useGlobalState, truncate  from '../store'

const Header = () => 
  const [theme, setTheme] = useState(localStorage.theme)
  const themeColor = theme === 'dark' ? 'light' : 'dark'
  const darken = theme === 'dark' ? true : false
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(() => 
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
  , [themeColor, theme])

  const toggleLight = () => 
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
    setTheme(themeColor)
  

  return (
    <header className="sticky top-0 z-50 dark:text-blue-500">
      <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]">
        <div className="px-6 w-full flex flex-wrap items-center justify-between">
          <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2">
            <Link
              to='/'
              className="flex flex-row justify-start items-center space-x-3"
            >
              <FaUserSecret className="cursor-pointer" size=25 />
              <span className="invisible md:visible dark:text-gray-300">
                Dominion
              </span>
            </Link>

            <div className="flex flex-row justify-center items-center space-x-5">
              darken ? (
                <MdLightMode
                  className="cursor-pointer"
                  size=25
                  onClick=toggleLight
                />
              ) : (
                <FaMoon
                  className="cursor-pointer"
                  size=25
                  onClick=toggleLight
                />
              )

              connectedAccount ? (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
                  focus:bg-blue-700 focus:shadow-lg focus:outline-none
                  focus:ring-0 active:bg-blue-800 active:shadow-lg
                  transition duration-150 ease-in-out dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                >
                  truncate(connectedAccount, 4, 4, 11)
                </button>
              ) : (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
                  focus:bg-blue-700 focus:shadow-lg focus:outline-none
                  focus:ring-0 active:bg-blue-800 active:shadow-lg
                  transition duration-150 ease-in-out dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                  onClick=connectWallet
                >
                  Connect Wallet
                </button>
              )
            </div>
          </div>
        </div>
      </nav>
    </header>
  )


export default Header

8.2 DAO应用横幅组件

该组件包含有关 DAO 当前状态的信息,例如总余额和未决提案的数量。

该组件还包括使用贡献函数生成新提案的能力。看看下面的代码。

import  useState  from 'react'
import  setGlobalState, useGlobalState  from '../store'
import  performContribute  from '../Dominion'
import  toast  from 'react-toastify'

const Banner = () => 
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [proposals] = useGlobalState('proposals')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [balance] = useGlobalState('balance')
  const [mybalance] = useGlobalState('mybalance')
  const [amount, setAmount] = useState('')

  const onPropose = () => 
    if (!isStakeholder) return
    setGlobalState('createModal', 'scale-100')
  

  const onContribute = () => 
    if (!!!amount || amount == '') return
    toast.info('Contribution in progress...')

    performContribute(amount).then((bal) => 
      if (!!!bal.message) 
        setGlobalState('balance', Number(balance) + Number(bal))
        setGlobalState('mybalance', Number(mybalance) + Number(bal))
        setAmount('')
        toast.success('Contribution received')
      
    )
  

  const opened = () =>
    proposals.filter(
      (proposal) => new Date().getTime() < Number(proposal.duration + '000')
    ).length

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">
        opened() Proposalopened() == 1 ? '' : 's' Currenly Opened
      </h2>
      <p>
        Current DAO Balance: <strong>balance Eth</strong> <br />
        Your contributions:' '
        <span>
          <strong>mybalance Eth</strong>
          isStakeholder ? ', and you are now a stakeholder 😊' : null
        </span>
      </p>
      <hr className="my-6 border-gray-300 dark:border-gray-500" />
      <p>
        isStakeholder
          ? 'You can now raise proposals on this platform 😆'
          : 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'
      </p>
      <div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4">
        <input
          type="number"
          className="form-control block w-full px-3 py-1.5
          text-base font-normaltext-gray-700
          bg-clip-padding border border-solid border-gray-300
          rounded transition ease-in-out m-0 shadow-md
          focus:text-gray-500 focus:outline-none
          dark:border-gray-500 dark:bg-transparent"
          placeholder="e.g 2.5 Eth"
          onChange=(e) => setAmount(e.target.value)
          value=amount
          required
        />
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        <button
          type="button"
          className=`inline-block px-6 py-2.5
          bg-blue-600 text-white font-medium text-xs
          leading-tight uppercase shadow-md rounded-full
          hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
          focus:shadow-lg focus:outline-none focus:ring-0
          active:bg-blue-800 active:shadow-lg transition
          duration-150 ease-in-out dark:text-blue-500
          dark:border dark:border-blue-500 dark:bg-transparent`
          data-mdb-ripple="true"
          data-mdb-ripple-color="light"
          onClick=onContribute
        >
          Contribute
        </button>

        isStakeholder ? (
          <button
            type="button"
            className=`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:text-blue-500
            dark:border dark:border-blue-500 dark:bg-transparent`
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick=onPropose
          >
            Propose
          </button>
        ) : null
        currentUser &&
        currentUser.uid == connectedAccount.toLowerCase() ? null : (
          <button
            type="button"
            className=`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:border dark:border-blue-500`
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick=() => setGlobalState('loginModal', 'scale-100')
          >
            Login Chat
          </button>
        )
      </div>
    </div>
  )


export default Banner

8.3 DAO应用提案组件

该组件包含我们智能合约中的提案列表。此外,使您能够在关闭和打开的提案之间进行过滤。在提案到期时,支付按钮变为可用,
该按钮使利益相关者可以选择支付与提案相关的金额。请参阅下面的代码。

import Identicon from 'react-identicons'
import  useState  from 'react'
import  Link  from 'react-router-dom'
import  truncate, useGlobalState, daysRemaining  from '../store'
import  payoutBeneficiary  from '../Dominion'
import  toast  from 'react-toastify'

const Proposals = () => 
  const [data] = useGlobalState('proposals')
  const [proposals, setProposals] = useState(data)

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  const getAll = () => setProposals(data)

  const getOpened = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() < Number(proposal.duration + '000')
      )
    )

  const getClosed = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() > Number(proposal.duration + '000')
      )
    )

  const handlePayout = (id) => 
    payoutBeneficiary(id).then((res) => 
      if (!!!res.code) 
        toast.success('Beneficiary successfully Paid Out!')
        window.location.reload()
      
    )
  

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className=`rounded-l-full px-6 py-2.5 $active`
          onClick=getAll
        >
          All
        </button>
        <button
          aria-current="page"
          className=`px-6 py-2.5 $deactive`
          onClick=getOpened
        >
          Open
        </button>
        <button
          aria-current="page"
          className=`rounded-r-full px-6 py-2.5 $deactive`
          onClick=getClosed
        >
          Closed
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Created By
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Title
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Expires
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Action
                  </th>
                </tr>
              </thead>
              <tbody>
                proposals.map((proposal) => (
                  <tr
                    key=proposal.id
                    className="border-b dark:border-gray-500"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string=proposal.proposer.toLowerCase()
                          size=25
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>truncate(proposal.proposer, 4, 4, 11)</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      proposal.title.substring(0, 80) + '...'
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      new Date().getTime() > Number(proposal.duration + '000')
                        ? 'Expired'
                        : daysRemaining(proposal.duration)
                    </td>
                    <td
                      className="flex justify-start items-center space-x-3
                      text-sm font-light px-6 py-4 whitespace-nowrap"
                    >
                      <Link
                        to='/proposal/' + proposal.id
                        className="dark:border rounded-full px-6 py-2.5 dark:border-blue-600
                          dark:text-blue-600 dark:bg-transparent font-medium text-xs leading-tight
                          uppercase hover:border-blue-700 focus:border-blue-700
                          focus:outline-none focus:ring-0 active:border-blue-800
                          transition duration-150 ease-in-out text-white bg-blue-600"
                      >
                        View
                      </Link>

                      new Date().getTime() >
                      Number(proposal.duration + '000') ? (
                        proposal.upvotes > proposal.downvotes ? (
                          !proposal.paid ? (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                uppercase hover:border-red-700 focus:border-red-700
                                focus:outline-none focus:ring-0 active:border-red-800
                                transition duration-150 ease-in-out text-white bg-red-600"
                              onClick=() => handlePayout(proposal.id)
                            >
                              Payout
                            </button>
                          ) : (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-green-600
                                  dark:text-green-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-green-700 focus:border-green-700
                                  focus:outline-none focus:ring-0 active:border-green-800
                                  transition duration-150 ease-in-out text-white bg-green-600"
                            >
                              Paid
                            </button>
                          )
                        ) : (
                          <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                  dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-red-700 focus:border-red-700
                                  focus:outline-none focus:ring-0 active:border-red-800
                                  transition duration-150 ease-in-out text-white bg-red-600"
                            >
                              Rejected
                            </button>
                        )
                      ) : null
                    </td>
                  </tr>
                ))
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  )


export default Proposals

8.4 DAO应用提案详情组件

此组件显示有关当前提案的信息,包括成本。该组件允许利益相关者接受或拒绝提案。

提议者可以组群,其他平台用户可以进行 web3.0 风格的匿名聊天。

该组件还包括一个条形图,可让你查看接受者与拒绝者的比率。看看下面的代码。

import moment from 'moment'
import  useEffect, useState  from 'react'
import  useParams, useNavigate  from 'react-router-dom'
import  toast  from 'react-toastify'
import  getGroup, createNewGroup, joinGroup  from '../CometChat'
import 
  BarChart,
  Bar,
  CartesianGrid,
  XAxis,
  YAxis,
  Legend,
  Tooltip,
 from 'recharts'
import  getProposal, voteOnProposal  from '../Dominion'
import  useGlobalState  from '../store'

const ProposalDetails = () => 
  const  id  = useParams()
  const navigator = useNavigate()
  const [proposal, setProposal] = useState(null)
  const [group, setGroup] = useState(null)
  const [data, setData] = useState([])
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')

  useEffect(() => 
    retrieveProposal()
    getGroup(`pid_$id`).then((group) => 
      if (!!!group.code) setGroup(group)
      console.log(group)
    )
  , [id])

  const retrieveProposal = () => 
    getProposal(id).then((res) => 
      setProposal(res)
      setData([
        
          name: 'Voters',
          Acceptees: res?.upvotes,
          Rejectees: res?.downvotes,
        ,
      ])
    )
  

  const onVote = (choice) => 
    if (new Date().getTime() > Number(proposal.duration + '000')) 
      toast.warning('Proposal expired!')
      return
    

    voteOnProposal(id, choice).then((res) => 
      if (!!!res.code) 
        toast.success('Voted successfully!')
        window.location.reload()
      
    )
  

  const daysRemaining = (days) => 
    const todaysdate = moment()
    days = Number((days + '000').slice(0))
    days = moment(days).format('YYYY-MM-DD')
    days = moment(days)
    days = days.diff(todaysdate, 'days')
    return days == 1 ? '1 day' : days + ' days'
  

  const onEnterChat = () => 
    if (group.hasJoined) 
      navigator(`/chat/$`pid_$id``)
     else 
      joinGroup(`pid_$id`).then((res) => 
        if (!!res) 
          navigator(`/chat/$`pid_$id``)
          console.log('Success joining: ', res)
         else 
          console.log('Error Joining Group: ', res)
        
      )
    
  

  const onCreateGroup = () => 
    createNewGroup(`pid_$id`, proposal.title).then((group) => 
      if (!!!group.code) 
        toast.success('Group created successfully!')
        setGroup(group)
       else 
        console.log('Error Creating Group: ', group)
      
    )
  

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">proposal?.title</h2>
      <p>
        This proposal is to payout <strong>proposal?.amount Eth</strong> and
        currently have' '
        <strong>proposal?.upvotes + proposal?.downvotes votes</strong> and
        will expire in <strong>daysRemaining(proposal?.duration)</strong>
      </p>
      <hr className="my-6 border-gray-300" />
      <p>proposal?.description</p>
      <div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto">
        <BarChart width=730 height=250 data=data>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="Acceptees" fill="#2563eb" />
          <Bar dataKey="Rejectees" fill="#dc2626" />
        </BarChart>
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        isStakeholder ? (
          <>
            <button
              type="button"
              className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
              leading-tight uppercase rounded-full shadow-md
              hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
              focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-blue-800 active:shadow-lg transition
              duration-150 ease-in-out dark:text-gray-300
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick=() => onVote(true)
            >
              Accept
            </button>
            <button
              type="button"
              className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
              leading-tight uppercase rounded-full shadow-md
              hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
              focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-blue-800 active:shadow-lg transition
              duration-150 ease-in-out
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick=() => onVote(false)
            >
              Reject
            </button>

            currentUser &&
            currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() &&
            !group ? (
              <button
                type="button"
                className="inline-block px-6 py-2.5
                bg-blue-600 text-white font-medium text-xs
                leading-tight uppercase rounded-full shadow-md
                hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0
                active:bg-blue-800 active:shadow-lg transition
                duration-150 ease-in-out
                dark:border dark:border-blue-500"
                data-mdb-ripple="true"
                data-mdb-ripple-color="light"
                onClick=onCreateGroup
              >
                Create Group
              </button>
            ) : null
          </>
        ) : null

        currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase rounded-full shadow-md
            hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-blue-800 active:shadow-lg transition
            duration-150 ease-in-out
            dark:border dark:border-blue-500"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick=onEnterChat
          >
            Chat
          </button>
        ) : null

        proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() &&
        !!!group ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5 bg-blue-600
            dark:bg-transparent text-white font-medium text-xs
            leading-tight uppercase rounded-full shadow-md
            hover:border-blue-700 hover:shadow-lg focus:border-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:border-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:text-blue-500
            dark:border dark:border-blue-500 disabled:bg-blue-300"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            disabled
          >
            Group N/A
          </button>
        ) : null
      </div>
    </div>
  )


export default ProposalDetails

8.5 DAO应用选民组件

该组件仅列出对提案进行投票的利益相关者。该组件还为用户提供了在拒绝者和接受者之间进行过滤的机会。请参阅下面的代码。

import Identicon from 'react-identicons'
import moment from 'moment'
import  useState, useEffect  from 'react'
import  useParams  from 'react-router-dom'
import  truncate  from '../store'
import  listVoters  from '../Dominion'

const Voters = () => 
  const [voters, setVoters] = useState([])
  const [data, setData] = useState([])
  const  id  = useParams()

  const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow()

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  useEffect(() => 
    listVoters(id).then((res) => 
      setVoters(res)
      setData(res)
    )
  , [id])

  const getAll = () => setVoters(data)

  const getAccepted = () => setVoters(data.filter((vote) => vote.choosen))

  const getRejected = () => setVoters(data.filter((vote) => !vote.choosen))

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className=`rounded-l-full px-6 py-2.5 $active`
          onClick=getAll
        >
          All
        </button>
        <button
          aria-current="page"
          className=`px-6 py-2.5 $deactive`
          onClick=getAccepted
        >
          Acceptees
        </button>
        <button
          aria-current="page"
          className=`rounded-r-full px-6 py-2.5 $deactive`
          onClick=getRejected
        >
          Rejectees
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto  shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voter
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voted
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Vote
                  </th>
                </tr>
              </thead>
              <tbody>
                voters.map((voter, i) => (
                  <tr
                    key=i
                    className="border-b dark:border-gray-500 transition duration-300 ease-in-out"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string=voter.voter.toLowerCase()
                          size=25
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>truncate(voter.voter, 4, 4, 11)</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      timeAgo(voter.timestamp)
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      voter.choosen ? (
                        <button
                          className

以上是关于DAO开发教程WEB3.0的主要内容,如果未能解决你的问题,请参考以下文章

IPFS&Filecoin一直为Web3.0作出巨大的努力和贡献

一键取消生效提案 Venus暴露DAO治理困局

关于区块链Web3.0智能合约DAppDAO一文解释清楚

Web3.0 博客DApp开发实战2022

从Dao聊到Aragon

Web3.0是什么?带你解析Web3.0