7

Web3.0 DAO开发教程【源码】

 2 years ago
source link: http://blog.hubwiz.com/2022/07/31/web3-dao-dev-tutorial/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

区块链开发课程精选

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

cover.avif

用熟悉的语言学习 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

2、安装开发依赖

2.1 NodeJs安装

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

web3 dao开发教程

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仪表板并创建一个帐户。

web3 dao开发教程

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

web3 dao开发教程

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

web3 dao开发教程
web3 dao开发教程

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

web3 dao开发教程

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

web3 dao开发教程

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

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

4、配置 Infura

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

web3 dao开发教程

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

web3 dao开发教程
web3 dao开发教程

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

web3 dao开发教程

接下来,添加你的 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已选择作为测试网络。

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

web3 dao开发教程

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

web3 dao开发教程

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

web3 dao开发教程

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

web3 dao开发教程
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).call{value: 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).call{value: 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应用标题栏组件

web3 dao开发教程

web3 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应用横幅组件

web3 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()} 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

8.3 DAO应用提案组件

web3 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 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应用选民组件

web3 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="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

8.6 DAO应用消息组件

web3 dao开发教程

借助 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

8.7 创建DAO提案组件

web3 dao开发教程

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

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

8.8 DAO应用认证组件

web3 dao开发教程

该组件可帮助你参与聊天功能。如果你已经注册,则需要创建一个帐户或登录。通过登录,可以参与群聊,并在 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

太棒了,让我们确保观点得到很好的体现……

8.9 DAO应用主页视图

web3 dao开发教程

此视图包括用于提供卓越 DAO 用户体验的header、banner和组件。proposals我们还使用 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

8.10 DAO应用提案视图

web3 dao开发教程

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

import Header from '../components/Header'
import ProposalDetails from '../components/ProposalDetails'
import Voters from '../components/Voters'
const Proposal = () => {
return (
<>
<Header />
<ProposalDetails />
<Voters />
</>
)
}
export default Proposal

8.11 DAO应用聊天视图

web3 dao开发教程

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

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文件。

8.12 DAO应用App组件

App 组件:用下面的代码替换 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

8.13 DAO应用入口脚本

在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,
}

9、启动DAO应用开发环境

第 1 步:使用下面的命令启动ganache-cli:

ganache-cli -a

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

web3 dao开发教程

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

web3 dao开发教程

第 3 步: 单击帐户图标并选择导入帐户。

web3 dao开发教程

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

web3 dao开发教程

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

web3 dao开发教程

10、DAO智能合约部署

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

truffle migrate
# or
truffle migrate --network rinkeby

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

接下来,打开另一个终端并使用yarn start启动项目。

完成!!!


原文链接:How to Build a Glorious Web3.0 DAO with React, Solidity, and CometChat

汇智网翻译整理,转载请标明出处


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK