如何使用 React、Solidity 和 CometChat 构建一个酷炫的 Web3.0 DAO

https://img.chengxuka.com

您将要构建的内容,请在此处查看演示git 存储库……

https://img.chengxuka.com

https://img.chengxuka.com

介绍

很高兴向您发布这个 web3.0 版本,也许您一直在寻找一个很好的例子来帮助您开始开发去中心化应用程序。

在本文中,您将逐步学习如何实现具有匿名聊天功能的去中心化自治组织 (DAO)。

先决条件

您将需要安装以下工具才能成功的构建:

  • Node
  • Ganache-Cli
  • Truffle
  • React
  • Infuria
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

安装依赖

NodeJs 安装

确保您的机器上已经安装了 NodeJs,如果还没有,请从HERE安装它。接下来,在终端上运行代码以确认它已安装。

https://img.chengxuka.com

Yarn、Ganache-cli 和 Truffle 安装

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

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

克隆 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 智能合约。

配置 CometChat SDK

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

步骤1:

前往CometChat ,并创建一个帐户。

https://img.chengxuka.com

第2步:

在注册后登录CometChat

https://img.chengxuka.com

第 3 步:

在面板中,添加一个名为dominionDAO 的新应用程序。

https://img.chengxuka.com

https://img.chengxuka.com

第4步:

从列表中选择您刚刚创建的应用程序。

https://img.chengxuka.com

第 5 步:

从快速入门中将 APP_ID、REGION 和 AUTH_KEY 复制到您的 .env 文件中。请参阅下图和代码。

https://img.chengxuka.com

将 REACT_COMET_CHAT 占位符替换为相应的值。

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

配置 Infuria 应用程序

步骤1:

前往Infuria并创建一个帐户。

https://img.chengxuka.com

第2步:

创建一个新项目。

https://img.chengxuka.com

https://img.chengxuka.com

第 3 步:

将 Rinkeby 测试网络 WebSocket 端点 URL 复制到您的 .env 文件。

https://img.chengxuka.com

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

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

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

访问您的 Metamask 私钥

步骤1:

单击您的 Metamask 浏览器扩展,并确保选择 Rinkeby 作为测试网络。接下来,在首选帐户上,单击三个点并选择 account details 。见下图。

https://img.chengxuka.com

第2步:

在提供的输入框中输入您的密码,然后单击确认按钮,这样您能够访问您的帐户私钥。

https://img.chengxuka.com

第 3 步:

单击“export private key”查看您的私钥。千万注意永远不要在 Github 等公共页面上公开您的密钥。这就是为什么我们将其附加为环境变量。

https://img.chengxuka.com

第4步:

将您的私钥复制到您的 .env 文件中。请参阅下图和代码:

https://img.chengxuka.com

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 密码短语粘贴到环境文件提供的对应位置上。

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).call{value: amount}("");
        require(success, "Payment failed");
        return true;
    }
}

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

解释:

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

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

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

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

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;

我们为利益相关者和贡献者角色设置了一些状态变量,并指定最短投票持续时间为一周。我们还初始化了总提案计数器 totalProposals 和一个变量 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;

raiseProposals 记录提交给我们智能合约的所有提案。stakeholderVotes,顾名思义,记录利益相关者的投票。votedOn 记录与提案相关的所有投票。contributors 记录向我们平台捐款的任何人,另一方面,stakeholders 记录贡献了超过 1 个以太币的人。

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 描述了每个投票的内容。

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

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

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
);
}

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

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 表示拒绝。

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];
}

此函数按 Id 检索特定提案。

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;
}

如果用户是利益相关者,则返回 True 或 False。

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;
}

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

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

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

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

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

配置部署脚本

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

在项目头上的 migrations 文件夹中,>> **2_deploy_contracts.js,**并使用下面的代码片段对其进行更新。

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

到这里,我们就完成了应用程序的智能合约,是时候开始构建 Dapp 界面了。

开发前端

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

Header 组件

https://img.chengxuka.com

https://img.chengxuka.com

该组件捕获有关当前用户的信息,并带有一个用于明暗模式的主题切换按钮。如果您想知道我是如何做到这一点的,那是通过 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

Banner组件

https://img.chengxuka.com

该组件包含有关 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()} Proposal{opened() == 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

Proposals组件

https://img.chengxuka.com

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

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

提案详细信息组件

https://img.chengxuka.com

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

提议者可以组群,其他平台用户可以进行 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

选民组件

https://img.chengxuka.com

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

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="border-2 rounded-full px-6 py-2.5 border-blue-600
                          text-blue-600 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"
                        >
                          Accepted
                        </button>
                      ) : (
                        <button
                          className="border-2 rounded-full px-6 py-2.5 border-red-600
                          text-red-600 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"
                        >
                          Rejected
                        </button>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div className="mt-4 text-center">
        {voters.length >= 10 ? (
          <button
            aria-current="page"
            className="rounded-full px-6 py-2.5 bg-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-800
            transition duration-150 ease-in-out dark:text-gray-300
            dark:border dark:border-gray-500 dark:bg-transparent"
          >
            Load More
          </button>
        ) : null}
      </div>
    </div>
  )
}

export default Voters

消息组件

https://img.chengxuka.com

借助 CometChat SDK 结合该组件的强大功能,用户可以匿名进行一对多聊天。贡献者和利益相关者可以在此处在其决策过程中进一步讨论提案。所有用户都保持匿名,并由他们的标识图标表示。

import Identicon from 'react-identicons'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { getMessages, sendMessage, CometChat } from '../CometChat'

const Messages = ({ gid }) => {
  const navigator = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  useEffect(() => {
    getMessages(gid).then((msgs) => {
      if (!!!msgs.code)
        setMessages(msgs.filter((msg) => msg.category == 'message'))
    })
    listenForMessage(gid)
  }, [gid])

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      })
    )
  }

  const handleMessage = (e) => {
    e.preventDefault()
    sendMessage(gid, message).then((msg) => {
      if (!!!msg.code) {
        setMessages((prevState) => [...prevState, msg])
        setMessage('')
        scrollToEnd()
      }
    })
  }

  const scrollToEnd = () => {
    const elmnt = document.getElementById('messages-container')
    elmnt.scrollTop = elmnt.scrollHeight
  }

  const dateToTime = (date) => {
    let hours = date.getHours()
    let minutes = date.getMinutes()
    let ampm = hours >= 12 ? 'pm' : 'am'
    hours = hours % 12
    hours = hours ? hours : 12
    minutes = minutes < 10 ? '0' + minutes : minutes
    let strTime = hours + ':' + minutes + ' ' + ampm
    return strTime
  }

  return (
    <div className="p-8">
      <div className="flex flex-row justify-start">
        <button
          className="px-4 py-2.5 bg-transparent hover:text-white
        font-bold 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"
        onClick={() => navigator(`/proposal/${gid.substr(4)}`)}
        >
          Exit Chat
        </button>
      </div>

      <div
        id="messages-container"
        className="h-[calc(100vh_-_16rem)] overflow-y-auto sm:pr-4 my-3"
      >
        {messages.map((message, i) =>
          message.sender.uid.toLowerCase() != connectedAccount.toLowerCase() ? (
            <div key={i} className="flex flex-row justify-start my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={message.sender.uid.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@{truncate(message.sender.uid, 4, 4, 11)}</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          ) : (
            <div key={i} className="flex flex-row justify-end my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md shadow-blue-300">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={connectedAccount.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@you</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          )
        )}
      </div>

      <form onSubmit={handleMessage} className="flex flex-row">
        <input
          className="w-full bg-transparent rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          required
        />
        <button type="submit" hidden>
          send
        </button>
      </form>
    </div>
  )
}

export default Messages

创建提案组件

https://img.chengxuka.com

该组件只是让您通过提供上图中所示字段的信息来提出建议。请参阅下面的代码。

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { raiseProposal } from '../Dominion'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const CreateProposal = () => {
  const [createModal] = useGlobalState('createModal')
  const [title, setTitle] = useState('')
  const [amount, setAmount] = useState('')
  const [beneficiary, setBeneficiary] = useState('')
  const [description, setDescription] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!title || !description || !beneficiary || !amount) return
    const proposal = { title, description, beneficiary, amount }

    raiseProposal(proposal).then((proposed) => {
      if (proposed) {
        toast.success('Proposal created, reloading in progress...')
        closeModal()
        window.location.reload()
      }
    })
  }

  const closeModal = () => {
    setGlobalState('createModal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setTitle('')
    setAmount('')
    setBeneficiary('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform z-50
      transition-transform duration-300 ${createModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Raise Proposal</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="title"
              placeholder="Title"
              onChange={(e) => setTitle(e.target.value)}
              value={title}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="amount"
              placeholder="e.g 2.5 Eth"
              onChange={(e) => setAmount(e.target.value)}
              value={amount}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="beneficiary"
              placeholder="Beneficiary Address"
              onChange={(e) => setBeneficiary(e.target.value)}
              value={beneficiary}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
              bg-transparent border-0
              focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            className="rounded-lg px-6 py-2.5 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 mt-5"
            onClick={handleSubmit}
          >
            Submit Proposal
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProposal

认证组件

https://img.chengxuka.com

该组件可帮助您参与聊天功能。如果您已经注册,则需要创建一个帐户或登录。通过登录,您可以参与群聊,并在 web3.0 样式的提案中与其他参与者进行匿名交谈。请参阅下面的代码。

import { FaTimes } from 'react-icons/fa'
import { loginWithCometChat, signInWithCometChat } from '../CometChat'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const ChatLogin = () => {
  const [loginModal] = useGlobalState('loginModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleSignUp = () => {
    signInWithCometChat(connectedAccount, connectedAccount).then((user) => {
      if (!!!user.code) {
        toast.success('Account created, now click the login button.')
      } else {
        toast.error(user.message)
      }
    })
  }

  const handleLogin = () => {
    loginWithCometChat(connectedAccount).then((user) => {
      if (!!!user.code) {
        setGlobalState('currentUser', user)
        toast.success('Logged in successful!')
        closeModal()
      } else {
        toast.error(user.message)
      }
    })
  }

  const closeModal = () => {
    setGlobalState('loginModal', 'scale-0')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform z-50
      transition-transform duration-300 ${loginModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Authenticate</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="my-2 font-light">
            <span>
              Once you login, you will be enabled to chat with other
              stakeholders to make a well-informed voting.
            </span>
          </div>

          <div
            className="flex flex-row justify-between items-center mt-2"
            role="group"
          >
            <button
              className="rounded-lg px-6 py-2.5 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 mt-5"
              onClick={handleLogin}
            >
              Login
            </button>

            <button
              className="rounded-lg px-6 py-2.5 bg-transparent
              text-blue-600 font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 hover:text-white focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5
              border-blue-600"
              onClick={handleSignUp}
            >
              Create Account
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ChatLogin

非常不错,让我们确保能够很好的表达视图……

主页视图

https://img.chengxuka.com

此视图包括标题、横幅和提案组件,用于提供特殊的 DAO 用户体验。我们还使用 Tailwind CSS 的强大功能来实现这种外观。看一下下面的代码。

import Banner from '../components/Banner'

import ChatLogin from '../components/ChatLogin'

import CreateProposal from '../components/CreateProposal'

import Header from '../components/Header'

import Proposals from '../components/Proposals'

const Home = () => {

return (

<>

<Header />

<Banner />

<Proposals />

<CreateProposal />

<ChatLogin />

</>

)

}

export default Home

提案视图

https://img.chengxuka.com

此视图将标头、提案详细信息和投票人组件耦合在一起,以平滑呈现单个组件。请参阅下面的代码。

import Header from '../components/Header'

import ProposalDetails from '../components/ProposalDetails'

import Voters from '../components/Voters'

const Proposal = () => {

return (

<>

<Header />

<ProposalDetails />

<Voters />

</>

)

}
export default Proposal

聊天视图

https://img.chengxuka.com

最后,聊天视图包含标题和消息组件,用于呈现高质量的聊天界面。请参阅下面的代码。

import { useParams, useNavigate } from 'react-router-dom'

import { useEffect, useState } from 'react'

import { getGroup } from '../CometChat'

import { toast } from 'react-toastify'

import Header from '../components/Header'

import Messages from '../components/Messages'

const Chat = () => {

const { gid } = useParams()

const navigator = useNavigate()

const [group, setGroup] = useState(null)

useEffect(() => {

getGroup(gid).then((group) => {

if (!!!group.code) {

setGroup(group)

} else {

toast.warning('Please join the group first!')

navigator(`/proposal/${gid.substr(4)}`)

}

})

}, [gid])

return (

<>

<Header />

<Messages gid={gid} />

</>

)

}

export default Chat

最后,别忘了更新 App.jsx 文件。

应用程序组件

用下面的代码替换 App 组件。

import { useEffect, useState } from 'react'

import { Routes, Route } from 'react-router-dom'

import { loadWeb3 } from './Dominion'

import { ToastContainer } from 'react-toastify'

import { isUserLoggedIn } from './CometChat'

import Home from './views/Home'

import Proposal from './views/Proposal'

import Chat from './views/Chat'

import 'react-toastify/dist/ReactToastify.min.css'

const App = () => {

const [loaded, setLoaded] = useState(false)

useEffect(() => {

loadWeb3().then((res) => {

if (res) setLoaded(true)

})

isUserLoggedIn()

}, [])

return (

<div className="min-h-screen bg-white text-gray-900 dark:bg-[#212936] dark:text-gray-300">

{loaded ? (

<Routes>

<Route path="/" element={<Home />} />

<Route path="proposal/:id" element={<Proposal />} />

<Route path="chat/:gid" element={<Chat />} />

</Routes>

) : null}

<ToastContainer

position="top-center"

autoClose={5000}

hideProgressBar={false}

newestOnTop={false}

closeOnClick

rtl={false}

pauseOnFocusLoss

draggable

pauseOnHover

/>

</div>

)

}

export default App

src >> 目录中,将以下代码粘贴到对应的文件中。

Index.jsx 文件

import React from 'react'

import ReactDOM from 'react-dom'

import { BrowserRouter } from 'react-router-dom'

import './index.css'

import App from './App'

import { initCometChat } from './CometChat'

initCometChat().then(() => {

ReactDOM.render(

<BrowserRouter>

<App />

</BrowserRouter>,

document.getElementById('root')

)

})

Index.css 文件

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');

* html {

padding: 0;

margin: 0;

box-sizing: border-box;

}

body {

margin: 0;

font-family: 'Open Sans', sans-serif;

-webkit-font-smoothing: antialiased;

-moz-osx-font-smoothing: grayscale;

}

@tailwind base;

@tailwind components;

@tailwind utilities;

CometChat.jsx

import Web3 from 'web3'
import { setGlobalState, getGlobalState } from './store'
import DominionDAO from './abis/DominionDAO.json'

const { ethereum } = window

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0])
  } catch (error) {
    console.log(JSON.stringify(error))
  }
}

const raiseProposal = async ({ title, description, beneficiary, amount }) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let proposal = await contract.methods
      .createProposal(title, description, beneficiary, amount)
      .send({ from: account })

    return proposal
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const performContribute = async (amount) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let balance = await contract.methods
      .contribute()
      .send({ from: account, value: amount })
    balance = window.web3.utils.fromWei(
      balance.events.Action.returnValues.amount
    )
    return balance
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const retrieveProposal = async (id) => {
  const web3 = window.web3
  try {
    const contract = getGlobalState('contract')
    const proposal = await contract.methods.getProposal(id).call().wait()
    return {
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }
  } catch (error) {
    console.log(error)
  }
}

const reconstructProposal = (proposal) => {
  return {
    id: proposal.id,
    amount: window.web3.utils.fromWei(proposal.amount),
    title: proposal.title,
    description: proposal.description,
    paid: proposal.paid,
    passed: proposal.passed,
    proposer: proposal.proposer,
    upvotes: Number(proposal.upvotes),
    downvotes: Number(proposal.downvotes),
    beneficiary: proposal.beneficiary,
    executor: proposal.executor,
    duration: proposal.duration,
  }
}

const getProposal = async (id) => {
  try {
    const proposals = getGlobalState('proposals')
    return proposals.find((proposal) => proposal.id == id)
  } catch (error) {
    console.log(error)
  }
}

const voteOnProposal = async (proposalId, supported) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const vote = await contract.methods
      .performVote(proposalId, supported)
      .send({ from: account })
    return vote
  } catch (error) {
    console.log(error)
    return error
  }
}

const listVoters = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const votes = await contract.methods.getVotesOf(id).call()
    return votes
  } catch (error) {
    console.log(error)
  }
}

const payoutBeneficiary = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const balance = await contract.methods
      .payBeneficiary(id)
      .send({ from: account })
    return balance
  } catch (error) {
    return error
  }
}

const loadWeb3 = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    window.web3 = new Web3(ethereum)
    await ethereum.request({ method: 'eth_requestAccounts' })
    window.web3 = new Web3(window.web3.currentProvider)

    const web3 = window.web3
    const accounts = await web3.eth.getAccounts()
    setGlobalState('connectedAccount', accounts[0])

    const networkId = await web3.eth.net.getId()
    const networkData = DominionDAO.networks[networkId]

    if (networkData) {
      const contract = new web3.eth.Contract(
        DominionDAO.abi,
        networkData.address
      )
      const isStakeholder = await contract.methods
        .isStakeholder()
        .call({ from: accounts[0] })
      const proposals = await contract.methods.getProposals().call()
      const balance = await contract.methods.daoBalance().call()
      const mybalance = await contract.methods
        .getBalance()
        .call({ from: accounts[0] })

      setGlobalState('contract', contract)
      setGlobalState('balance', web3.utils.fromWei(balance))
      setGlobalState('mybalance', web3.utils.fromWei(mybalance))
      setGlobalState('isStakeholder', isStakeholder)
      setGlobalState('proposals', structuredProposals(proposals))
    } else {
      window.alert('DominionDAO contract not deployed to detected network.')
    }
    return true
  } catch (error) {
    alert('Please connect your metamask wallet!')
    console.log(error)
    return false
  }
}

const structuredProposals = (proposals) => {
  const web3 = window.web3
  return proposals
    .map((proposal) => ({
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }))
    .reverse()
}

export {
  loadWeb3,
  connectWallet,
  performContribute,
  raiseProposal,
  retrieveProposal,
  voteOnProposal,
  getProposal,
  listVoters,
  payoutBeneficiary,
}

启动开发环境

步骤1:

使用以下命令使用 ganache-cli 启动一些测试帐户:

ganache-cli -a

这将创建一些测试账户,每个账户加载 100 个假的以太币,当然,这些仅用于测试目的。见下图:

https://img.chengxuka.com

第2步:

如下图所示,使用 Metamask 添加本地测试网络。

https://img.chengxuka.com

第 3 步:

单击帐户图标并选择导入帐户。

https://img.chengxuka.com

复制大约五个私钥,然后将它们一个接一个地添加到您的本地测试网络。见下图。

https://img.chengxuka.com

观察添加到本地测试网络的新帐户,预加载了 100 ETH。确保添加大约五个帐户,这样就可以测试了。见下图。

https://img.chengxuka.com

智能合约部署

现在打开一个新终端并运行以下命令。

truffle migrate
# or
truffle migrate --network rinkeby

上述命令会将您的智能合约部署到您的本地或 Infuria rinkeby 测试网络。

接下来,打开另一个终端并使用 yarn start 启动 react 应用程序。

结论

到此,我们就结束了这篇教程,关于如何开发一个去中心化应用程序。

原文链接:https://coinsbench.com/how-to-build-a-glorious-web3-0-dao-with-react-solidity-and-cometchat-4d9986b0a699

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值