DAO开发教程WEB3.0
Posted 新缸中之脑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DAO开发教程WEB3.0相关的知识,希望对你有一定的参考价值。
在这个教程中,我们将学习如何开发一个支持聊天的去中心化自治组织 (DAO),教程内容
涵盖使用的工具链、智能合约开发部署和前端应用开发。
用熟悉的语言学习 Web3.0 开发 :Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart
1、使用的工具链
我们需要安装以下工具来完成此教程:
- Node.js
- Ganache-Cli
- Truffle
- React
- Infura
- Tailwind CSS
- CometChat SDK
- Metamask
- Yarn
2、安装开发依赖
2.1 NodeJs安装
确保你的机器上已经安装了 NodeJs。接下来,在终端上运行代码以确认它已安装。
2.2 Yarn、Ganache-cli 和 Truffle 安装
在终端上运行以下代码以全局安装这些基本软件包。
npm i -g yarn
npm i -g truffle
npm i -g ganache-cli
2.3 克隆 Web3 入门项目
使用下面的命令,克隆下面的 web 3.0 入门项目。这将确保我们都在同一个页面上并使用相同的包。
git clone https://github.com/Daltonic/dominionDAO
太棒了,让我们用下面的文件替换package.json文件:
"name": "dominionDAO",
"private": true,
"version": "0.0.0",
"scripts":
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
,
"dependencies":
"@cometchat-pro/chat": "3.0.6",
"moment": "^2.29.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.3.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.0.1",
"recharts": "^2.1.9",
"web-vitals": "^2.1.4",
"web3": "^1.7.1"
,
"devDependencies":
"@openzeppelin/contracts": "^4.5.0",
"@tailwindcss/forms": "0.4.0",
"@truffle/hdwallet-provider": "^2.0.4",
"assert": "^2.0.0",
"autoprefixer": "10.4.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"babel-register": "^6.26.0",
"buffer": "^6.0.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.0",
"https-browserify": "^1.0.0",
"mnemonics": "^1.1.3",
"os-browserify": "^0.3.0",
"postcss": "8.4.5",
"process": "^0.11.10",
"react-app-rewired": "^2.1.11",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0"
,
"browserslist":
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
太好了,用上面的代码替换你的package.json文件,然后在你的终端上运行yarn install
。
安装完毕后,让我们开始编写 Dominion DAO 智能合约。
3、配置 CometChat SDK
要配置CometChat SDK,请按照以下步骤操作,最后,我们需要将这些密钥存储为环境变量。
第 1 步: 前往CometChat仪表板并创建一个帐户。
第 2步:在注册后登录 CometChat 仪表 板。
第 3 步: 在仪表板中,添加一个名为dominionDAO 的新应用程序。
第 4 步: 从列表中选择刚刚创建的应用程序。
第 5 步: 从快速入门中将APP_ID、REGION和AUTH_KEY, 复制到你的.env文件中。请参阅图像和代码片段。
将REACT_COMET_CHAT
占位符键替换为相应的值:
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
4、配置 Infura
第 1 步: 前往Infura创建一个帐户。
第 2 步: 从Infura仪表板创建一个新项目。
第 3 步: 将Rinkeby测试网络 WebSocket 端点 URL 复制到你的.env
文件中。
接下来,添加你的 Metamask 密码短语和首选帐户私钥。如果正确地完成了这些操作,你的环境变量现在应该如下所示。
ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
如果不知道如何访问你的私钥,请参阅下面的部分。
5、访问 Metamask 私钥
第 1 步: 单击Metamask浏览器扩展程序,并确保Rinkeby已选择作为测试网络。
接下来,在首选帐户上,单击垂直虚线并选择帐户详细信息。见下图。
第 2 步: 在提供的字段中输入你的密码,然后单击确认按钮,这将使你能够访问你的帐户私钥。
第 3 步: 单击“导出私钥”以查看你的私钥。确保永远不会在公共页面上公开你的密钥,例如Github. 这就是为什么我们将其附加为环境变量。
第 4 步: 将你的私钥复制到 .env
文件中。请参阅下面的图像和代码片段:
ENDPOINT_URL=***************************
SECRET_KEY=******************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
至于SECRET_KEY
,你需要将你的Metamask密码短语粘贴到环境文件中提供的空间中。
6、Dominion DAO 智能合约
这是智能合约的完整代码,我将逐个解释所有函数和变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract DominionDAO is ReentrancyGuard, AccessControl
bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;
mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;
struct ProposalStruct
uint256 id;
uint256 amount;
uint256 duration;
uint256 upvotes;
uint256 downvotes;
string title;
string description;
bool passed;
bool paid;
address payable beneficiary;
address proposer;
address executor;
struct VotedStruct
address voter;
uint256 timestamp;
bool choosen;
event Action(
address indexed initiator,
bytes32 role,
string message,
address indexed beneficiary,
uint256 amount
);
modifier stakeholderOnly(string memory message)
require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
_;
modifier contributorOnly(string memory message)
require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
_;
function createProposal(
string calldata title,
string calldata description,
address beneficiary,
uint256 amount
)external
stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
uint256 proposalId = totalProposals++;
ProposalStruct storage proposal = raisedProposals[proposalId];
proposal.id = proposalId;
proposal.proposer = payable(msg.sender);
proposal.title = title;
proposal.description = description;
proposal.beneficiary = payable(beneficiary);
proposal.amount = amount;
proposal.duration = block.timestamp + MIN_VOTE_DURATION;
emit Action(
msg.sender,
CONTRIBUTOR_ROLE,
"PROPOSAL RAISED",
beneficiary,
amount
);
function performVote(uint256 proposalId, bool choosen)
external
stakeholderOnly("Unauthorized: Stakeholders only")
ProposalStruct storage proposal = raisedProposals[proposalId];
handleVoting(proposal);
if (choosen) proposal.upvotes++;
else proposal.downvotes++;
stakeholderVotes[msg.sender].push(proposal.id);
votedOn[proposal.id].push(
VotedStruct(
msg.sender,
block.timestamp,
choosen
)
);
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"PROPOSAL VOTE",
proposal.beneficiary,
proposal.amount
);
function handleVoting(ProposalStruct storage proposal) private
if (
proposal.passed ||
proposal.duration <= block.timestamp
)
proposal.passed = true;
revert("Proposal duration expired");
uint256[] memory tempVotes = stakeholderVotes[msg.sender];
for (uint256 votes = 0; votes < tempVotes.length; votes++)
if (proposal.id == tempVotes[votes])
revert("Double voting not allowed");
function payBeneficiary(uint256 proposalId)
external
stakeholderOnly("Unauthorized: Stakeholders only")
returns (bool)
ProposalStruct storage proposal = raisedProposals[proposalId];
require(daoBalance >= proposal.amount, "Insufficient fund");
require(block.timestamp > proposal.duration, "Proposal still ongoing");
if (proposal.paid) revert("Payment sent before");
if (proposal.upvotes <= proposal.downvotes)
revert("Insufficient votes");
payTo(proposal.beneficiary, proposal.amount);
proposal.paid = true;
proposal.executor = msg.sender;
daoBalance -= proposal.amount;
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"PAYMENT TRANSFERED",
proposal.beneficiary,
proposal.amount
);
return true;
function contribute() payable external
if (!hasRole(STAKEHOLDER_ROLE, msg.sender))
uint256 totalContribution =
contributors[msg.sender] + msg.value;
if (totalContribution >= 5 ether)
stakeholders[msg.sender] = totalContribution;
contributors[msg.sender] += msg.value;
_setupRole(STAKEHOLDER_ROLE, msg.sender);
_setupRole(CONTRIBUTOR_ROLE, msg.sender);
else
contributors[msg.sender] += msg.value;
_setupRole(CONTRIBUTOR_ROLE, msg.sender);
else
contributors[msg.sender] += msg.value;
stakeholders[msg.sender] += msg.value;
daoBalance += msg.value;
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"CONTRIBUTION RECEIVED",
address(this),
msg.value
);
function getProposals()
external
view
returns (ProposalStruct[] memory props)
props = new ProposalStruct[](totalProposals);
for (uint256 i = 0; i < totalProposals; i++)
props[i] = raisedProposals[i];
function getProposal(uint256 proposalId)
external
view
returns (ProposalStruct memory)
return raisedProposals[proposalId];
function getVotesOf(uint256 proposalId)
external
view
returns (VotedStruct[] memory)
return votedOn[proposalId];
function getStakeholderVotes()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256[] memory)
return stakeholderVotes[msg.sender];
function getStakeholderBalance()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256)
return stakeholders[msg.sender];
function isStakeholder() external view returns (bool)
return stakeholders[msg.sender] > 0;
function getContributorBalance()
external
view
contributorOnly("Denied: User is not a contributor")
returns (uint256)
return contributors[msg.sender];
function isContributor() external view returns (bool)
return contributors[msg.sender] > 0;
function getBalance() external view returns (uint256)
return contributors[msg.sender];
function payTo(
address to,
uint256 amount
) internal returns (bool)
(bool success,) = payable(to).callvalue: amount("");
require(success, "Payment failed");
return true;
在刚刚克隆的项目中,前往src >> contract
目录并创建一个名为 DominionDAO.sol
的文件,然后将上述
代码粘贴到其中。
6.1 pragma语句
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
Solidity 需要一个许可证标识符来编译你的代码,否则它会产生一个警告,要求你指定一个。此外,Solidity 要求你为智能合约指定编译器的版本。这就是pragma
这个词所代表的。
6.2 import语句
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
在上面的代码块中,我们使用import
导入两个openzeppelin’s智能合约来指定角色并保护我们的智能合约免受重入攻击。
6.3 DAO角色相关的状态变量
bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;
我们为利益相关者和贡献者角色设置了一些状态变量,并将最短投票持续时间指定为一周。我们还初始化了总提案计数器和一个变量来记录我们的可用余额。
6.4 DAO提案和投票相关的状态变量
mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;
raisedProposals
跟踪提交给我们智能合约的所有提案。stakeholderVotes
顾名思义,跟踪利益相关者的投票。votedOn
跟踪与
提案相关的所有投票。贡献者跟踪向我们平台捐款的任何人,而1 ether
以上的贡献者被视为利益相关者进行跟踪。
6.5 DAO提案和投票的数据结构
struct ProposalStruct
uint256 id;
uint256 amount;
uint256 duration;
uint256 upvotes;
uint256 downvotes;
string title;
string description;
bool passed;
bool paid;
address payable beneficiary;
address proposer;
address executor;
struct VotedStruct
address voter;
uint256 timestamp;
bool choosen;
proposalStruct
描述每个提案的内容,而votedStruct
描述每个投票的内容。
6.6 Action事件
event Action(
address indexed initiator,
bytes32 role,
string message,
address indexed beneficiary,
uint256 amount
);
这是一个名为 Action 的动态事件。这将帮助我们丰富每笔交易注销的信息。
6.7 DAO角色相关的修饰符
modifier stakeholderOnly(string memory message)
require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
_;
modifier contributorOnly(string memory message)
require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
_;
上述修饰符帮助我们按角色识别用户,也可以防止他们访问一些未经授权的资源。
6.8 DAO提案创建方法
function createProposal(
string calldata title,
string calldata description,
address beneficiary,
uint256 amount
)external
stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
uint256 proposalId = totalProposals++;
ProposalStruct storage proposal = raisedProposals[proposalId];
proposal.id = proposalId;
proposal.proposer = payable(msg.sender);
proposal.title = title;
proposal.description = description;
proposal.beneficiary = payable(beneficiary);
proposal.amount = amount;
proposal.duration = block.timestamp + MIN_VOTE_DURATION;
emit Action(
msg.sender,
CONTRIBUTOR_ROLE,
"PROPOSAL RAISED",
beneficiary,
amount
);
上述函数获取提案的标题、描述、金额和受益人的钱包地址并创建提案。该功能仅允许利益相关者创建提案。利益相关者是至少做出了1 ether
贡献的用户。
6.9 DAO投票方法
function performVote(uint256 proposalId, bool choosen)
external
stakeholderOnly("Unauthorized: Stakeholders only")
ProposalStruct storage proposal = raisedProposals[proposalId];
handleVoting(proposal);
if (choosen) proposal.upvotes++;
else proposal.downvotes++;
stakeholderVotes[msg.sender].push(proposal.id);
votedOn[proposal.id].push(
VotedStruct(
msg.sender,
block.timestamp,
choosen
)
);
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"PROPOSAL VOTE",
proposal.beneficiary,
proposal.amount
);
此函数接受两个参数,一个提案 ID 和一个由布尔值表示的首选选项。True 表示接受投票,False 表示拒绝。
6.10 DAO投票执行方法
function handleVoting(ProposalStruct storage proposal) private
if (
proposal.passed ||
proposal.duration <= block.timestamp
)
proposal.passed = true;
revert("Proposal duration expired");
uint256[] memory tempVotes = stakeholderVotes[msg.sender];
for (uint256 votes = 0; votes < tempVotes.length; votes++)
if (proposal.id == tempVotes[votes])
revert("Double voting not allowed");
此函数执行实际投票,包括检查用户是否是利益相关者并有资格投票。
6.11 DAO受益人支付方法
function payBeneficiary(uint256 proposalId)
external
stakeholderOnly("Unauthorized: Stakeholders only")
returns (bool)
ProposalStruct storage proposal = raisedProposals[proposalId];
require(daoBalance >= proposal.amount, "Insufficient fund");
require(block.timestamp > proposal.duration, "Proposal still ongoing");
if (proposal.paid) revert("Payment sent before");
if (proposal.upvotes <= proposal.downvotes)
revert("Insufficient votes");
payTo(proposal.beneficiary, proposal.amount);
proposal.paid = true;
proposal.executor = msg.sender;
daoBalance -= proposal.amount;
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"PAYMENT TRANSFERED",
proposal.beneficiary,
proposal.amount
);
return true;
此功能负责根据特定标准向提案所附的受益人付款:
- 受益人不得已经支付。
- 案期限必须已过期。
- 可用余额必须能够支付给受益人。
- 票数不得平分。
6.12 DAO捐款方法
function contribute() payable external
if (!hasRole(STAKEHOLDER_ROLE, msg.sender))
uint256 totalContribution =
contributors[msg.sender] + msg.value;
if (totalContribution >= 5 ether)
stakeholders[msg.sender] = totalContribution;
contributors[msg.sender] += msg.value;
_setupRole(STAKEHOLDER_ROLE, msg.sender);
_setupRole(CONTRIBUTOR_ROLE, msg.sender);
else
contributors[msg.sender] += msg.value;
_setupRole(CONTRIBUTOR_ROLE, msg.sender);
else
contributors[msg.sender] += msg.value;
stakeholders[msg.sender] += msg.value;
daoBalance += msg.value;
emit Action(
msg.sender,
STAKEHOLDER_ROLE,
"CONTRIBUTION RECEIVED",
address(this),
msg.value
);
该函数负责从捐助者和有兴趣成为利益相关者的人那里收集捐款。
6.13 DAO提案查询方法
function getProposals()
external
view
returns (ProposalStruct[] memory props)
props = new ProposalStruct[](totalProposals);
for (uint256 i = 0; i < totalProposals; i++)
props[i] = raisedProposals[i];
上面函数检索记录在此智能合约上的一组提案。
6.14 DAO提案详情读取方法
function getProposal(uint256 proposalId)
external
view
returns (ProposalStruct memory)
return raisedProposals[proposalId];
上面函数按 Id 检索特定提案。
6.15 DAO投票查询方法
function getVotesOf(uint256 proposalId)
external
view
returns (VotedStruct[] memory)
return votedOn[proposalId];
这将返回与特定提案相关的投票列表。
6.16 DAO利益相关者投票查询方法
function getStakeholderVotes()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256[] memory)
return stakeholderVotes[msg.sender];
这将返回智能合约上的利益相关者列表,并且只有利益相关者才能调用此函数。
6.17 DAO利益相关者余额查询方法
function getStakeholderBalance()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256)
return stakeholders[msg.sender];
这将返回利益相关者贡献的金额。
6.18 DAO利益相关者判别方法
function isStakeholder() external view returns (bool)
return stakeholders[msg.sender] > 0;
判断用户是否为利益相关者,返回 True 或 False。
6.19 DAO一般贡献者余额查询方法
function getContributorBalance()
external
view
contributorOnly("Denied: User is not a contributor")
returns (uint256)
return contributors[msg.sender];
这将返回贡献者的余额,并且只有贡献者可以访问。
6.20 DAO一般贡献者判别方法
function isContributor() external view returns (bool)
return contributors[msg.sender] > 0;
这会检查用户是否是贡献者,并用 True 或 False 表示。
6.21 DAO普通用户余额查询方法
function getBalance() external view returns (uint256)
return contributors[msg.sender];
返回调用用户的余额,无论其角色如何。
6.22 DAO支付方法
function payTo(
address to,
uint256 amount
) internal returns (bool)
(bool success,) = payable(to).callvalue: amount("");
require(success, "Payment failed");
return true;
此函数执行指定金额和帐户的付款。
7、配置DAO合约部署脚本
与智能合约有关的另一件事是配置部署脚本。
前往项目的迁移文件夹中的 2_deploy_contracts.js
文件,并使用下面的代码片段对其进行更新。
const DominionDAO = artifacts.require('DominionDAO')
module.exports = async function (deployer)
await deployer.deploy(DominionDAO)
太棒了,我们刚刚完成了应用程序的智能合约,是时候开始构建 Dapp 界面了。
8、开发DAO应用前端
前端包括许多组件和部件。我们将创建所有组件、视图和其余外围设备。
8.1 DAO应用标题栏组件
该组件捕获有关当前用户的信息,并带有一个用于明暗模式的主题切换按钮。这是通过 Tailwind CSS 实现的,具体请参阅下面的代码。
import useState, useEffect from 'react'
import FaUserSecret from 'react-icons/fa'
import MdLightMode from 'react-icons/md'
import FaMoon from 'react-icons/fa'
import Link from 'react-router-dom'
import connectWallet from '../Dominion'
import useGlobalState, truncate from '../store'
const Header = () =>
const [theme, setTheme] = useState(localStorage.theme)
const themeColor = theme === 'dark' ? 'light' : 'dark'
const darken = theme === 'dark' ? true : false
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(() =>
const root = window.document.documentElement
root.classList.remove(themeColor)
root.classList.add(theme)
localStorage.setItem('theme', theme)
, [themeColor, theme])
const toggleLight = () =>
const root = window.document.documentElement
root.classList.remove(themeColor)
root.classList.add(theme)
localStorage.setItem('theme', theme)
setTheme(themeColor)
return (
<header className="sticky top-0 z-50 dark:text-blue-500">
<nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]">
<div className="px-6 w-full flex flex-wrap items-center justify-between">
<div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2">
<Link
to='/'
className="flex flex-row justify-start items-center space-x-3"
>
<FaUserSecret className="cursor-pointer" size=25 />
<span className="invisible md:visible dark:text-gray-300">
Dominion
</span>
</Link>
<div className="flex flex-row justify-center items-center space-x-5">
darken ? (
<MdLightMode
className="cursor-pointer"
size=25
onClick=toggleLight
/>
) : (
<FaMoon
className="cursor-pointer"
size=25
onClick=toggleLight
/>
)
connectedAccount ? (
<button
className="px-4 py-2.5 bg-blue-600 text-white
font-medium text-xs leading-tight uppercase
rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none
focus:ring-0 active:bg-blue-800 active:shadow-lg
transition duration-150 ease-in-out dark:text-blue-500
dark:border dark:border-blue-500 dark:bg-transparent"
>
truncate(connectedAccount, 4, 4, 11)
</button>
) : (
<button
className="px-4 py-2.5 bg-blue-600 text-white
font-medium text-xs leading-tight uppercase
rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none
focus:ring-0 active:bg-blue-800 active:shadow-lg
transition duration-150 ease-in-out dark:text-blue-500
dark:border dark:border-blue-500 dark:bg-transparent"
onClick=connectWallet
>
Connect Wallet
</button>
)
</div>
</div>
</div>
</nav>
</header>
)
export default Header
8.2 DAO应用横幅组件
该组件包含有关 DAO 当前状态的信息,例如总余额和未决提案的数量。
该组件还包括使用贡献函数生成新提案的能力。看看下面的代码。
import useState from 'react'
import setGlobalState, useGlobalState from '../store'
import performContribute from '../Dominion'
import toast from 'react-toastify'
const Banner = () =>
const [isStakeholder] = useGlobalState('isStakeholder')
const [proposals] = useGlobalState('proposals')
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
const [balance] = useGlobalState('balance')
const [mybalance] = useGlobalState('mybalance')
const [amount, setAmount] = useState('')
const onPropose = () =>
if (!isStakeholder) return
setGlobalState('createModal', 'scale-100')
const onContribute = () =>
if (!!!amount || amount == '') return
toast.info('Contribution in progress...')
performContribute(amount).then((bal) =>
if (!!!bal.message)
setGlobalState('balance', Number(balance) + Number(bal))
setGlobalState('mybalance', Number(mybalance) + Number(bal))
setAmount('')
toast.success('Contribution received')
)
const opened = () =>
proposals.filter(
(proposal) => new Date().getTime() < Number(proposal.duration + '000')
).length
return (
<div className="p-8">
<h2 className="font-semibold text-3xl mb-5">
opened() Proposalopened() == 1 ? '' : 's' Currenly Opened
</h2>
<p>
Current DAO Balance: <strong>balance Eth</strong> <br />
Your contributions:' '
<span>
<strong>mybalance Eth</strong>
isStakeholder ? ', and you are now a stakeholder 😊' : null
</span>
</p>
<hr className="my-6 border-gray-300 dark:border-gray-500" />
<p>
isStakeholder
? 'You can now raise proposals on this platform 😆'
: 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'
</p>
<div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4">
<input
type="number"
className="form-control block w-full px-3 py-1.5
text-base font-normaltext-gray-700
bg-clip-padding border border-solid border-gray-300
rounded transition ease-in-out m-0 shadow-md
focus:text-gray-500 focus:outline-none
dark:border-gray-500 dark:bg-transparent"
placeholder="e.g 2.5 Eth"
onChange=(e) => setAmount(e.target.value)
value=amount
required
/>
</div>
<div
className="flex flex-row justify-start items-center space-x-3 mt-4"
role="group"
>
<button
type="button"
className=`inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase shadow-md rounded-full
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out dark:text-blue-500
dark:border dark:border-blue-500 dark:bg-transparent`
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=onContribute
>
Contribute
</button>
isStakeholder ? (
<button
type="button"
className=`inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase shadow-md rounded-full
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out dark:text-blue-500
dark:border dark:border-blue-500 dark:bg-transparent`
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=onPropose
>
Propose
</button>
) : null
currentUser &&
currentUser.uid == connectedAccount.toLowerCase() ? null : (
<button
type="button"
className=`inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase shadow-md rounded-full
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out dark:border dark:border-blue-500`
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=() => setGlobalState('loginModal', 'scale-100')
>
Login Chat
</button>
)
</div>
</div>
)
export default Banner
8.3 DAO应用提案组件
该组件包含我们智能合约中的提案列表。此外,使您能够在关闭和打开的提案之间进行过滤。在提案到期时,支付按钮变为可用,
该按钮使利益相关者可以选择支付与提案相关的金额。请参阅下面的代码。
import Identicon from 'react-identicons'
import useState from 'react'
import Link from 'react-router-dom'
import truncate, useGlobalState, daysRemaining from '../store'
import payoutBeneficiary from '../Dominion'
import toast from 'react-toastify'
const Proposals = () =>
const [data] = useGlobalState('proposals')
const [proposals, setProposals] = useState(data)
const deactive = `bg-transparent
text-blue-600 font-medium text-xs leading-tight
uppercase hover:bg-blue-700 focus:bg-blue-700
focus:outline-none focus:ring-0 active:bg-blue-600
transition duration-150 ease-in-out overflow-hidden
border border-blue-600 hover:text-white focus:text-white`
const active = `bg-blue-600
text-white font-medium text-xs leading-tight
uppercase hover:bg-blue-700 focus:bg-blue-700
focus:outline-none focus:ring-0 active:bg-blue-800
transition duration-150 ease-in-out overflow-hidden
border border-blue-600`
const getAll = () => setProposals(data)
const getOpened = () =>
setProposals(
data.filter(
(proposal) => new Date().getTime() < Number(proposal.duration + '000')
)
)
const getClosed = () =>
setProposals(
data.filter(
(proposal) => new Date().getTime() > Number(proposal.duration + '000')
)
)
const handlePayout = (id) =>
payoutBeneficiary(id).then((res) =>
if (!!!res.code)
toast.success('Beneficiary successfully Paid Out!')
window.location.reload()
)
return (
<div className="flex flex-col p-8">
<div className="flex flex-row justify-center items-center" role="group">
<button
aria-current="page"
className=`rounded-l-full px-6 py-2.5 $active`
onClick=getAll
>
All
</button>
<button
aria-current="page"
className=`px-6 py-2.5 $deactive`
onClick=getOpened
>
Open
</button>
<button
aria-current="page"
className=`rounded-r-full px-6 py-2.5 $deactive`
onClick=getClosed
>
Closed
</button>
</div>
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
<div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md">
<table className="min-w-full">
<thead className="border-b dark:border-gray-500">
<tr>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Created By
</th>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Title
</th>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Expires
</th>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Action
</th>
</tr>
</thead>
<tbody>
proposals.map((proposal) => (
<tr
key=proposal.id
className="border-b dark:border-gray-500"
>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
<div className="flex flex-row justify-start items-center space-x-3">
<Identicon
string=proposal.proposer.toLowerCase()
size=25
className="h-10 w-10 object-contain rounded-full mr-3"
/>
<span>truncate(proposal.proposer, 4, 4, 11)</span>
</div>
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
proposal.title.substring(0, 80) + '...'
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
new Date().getTime() > Number(proposal.duration + '000')
? 'Expired'
: daysRemaining(proposal.duration)
</td>
<td
className="flex justify-start items-center space-x-3
text-sm font-light px-6 py-4 whitespace-nowrap"
>
<Link
to='/proposal/' + proposal.id
className="dark:border rounded-full px-6 py-2.5 dark:border-blue-600
dark:text-blue-600 dark:bg-transparent font-medium text-xs leading-tight
uppercase hover:border-blue-700 focus:border-blue-700
focus:outline-none focus:ring-0 active:border-blue-800
transition duration-150 ease-in-out text-white bg-blue-600"
>
View
</Link>
new Date().getTime() >
Number(proposal.duration + '000') ? (
proposal.upvotes > proposal.downvotes ? (
!proposal.paid ? (
<button
className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
uppercase hover:border-red-700 focus:border-red-700
focus:outline-none focus:ring-0 active:border-red-800
transition duration-150 ease-in-out text-white bg-red-600"
onClick=() => handlePayout(proposal.id)
>
Payout
</button>
) : (
<button
className="dark:border rounded-full px-6 py-2.5 dark:border-green-600
dark:text-green-600 dark:bg-transparent font-medium text-xs leading-tight
uppercase hover:border-green-700 focus:border-green-700
focus:outline-none focus:ring-0 active:border-green-800
transition duration-150 ease-in-out text-white bg-green-600"
>
Paid
</button>
)
) : (
<button
className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
uppercase hover:border-red-700 focus:border-red-700
focus:outline-none focus:ring-0 active:border-red-800
transition duration-150 ease-in-out text-white bg-red-600"
>
Rejected
</button>
)
) : null
</td>
</tr>
))
</tbody>
</table>
</div>
</div>
</div>
</div>
)
export default Proposals
8.4 DAO应用提案详情组件
此组件显示有关当前提案的信息,包括成本。该组件允许利益相关者接受或拒绝提案。
提议者可以组群,其他平台用户可以进行 web3.0 风格的匿名聊天。
该组件还包括一个条形图,可让你查看接受者与拒绝者的比率。看看下面的代码。
import moment from 'moment'
import useEffect, useState from 'react'
import useParams, useNavigate from 'react-router-dom'
import toast from 'react-toastify'
import getGroup, createNewGroup, joinGroup from '../CometChat'
import
BarChart,
Bar,
CartesianGrid,
XAxis,
YAxis,
Legend,
Tooltip,
from 'recharts'
import getProposal, voteOnProposal from '../Dominion'
import useGlobalState from '../store'
const ProposalDetails = () =>
const id = useParams()
const navigator = useNavigate()
const [proposal, setProposal] = useState(null)
const [group, setGroup] = useState(null)
const [data, setData] = useState([])
const [isStakeholder] = useGlobalState('isStakeholder')
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
useEffect(() =>
retrieveProposal()
getGroup(`pid_$id`).then((group) =>
if (!!!group.code) setGroup(group)
console.log(group)
)
, [id])
const retrieveProposal = () =>
getProposal(id).then((res) =>
setProposal(res)
setData([
name: 'Voters',
Acceptees: res?.upvotes,
Rejectees: res?.downvotes,
,
])
)
const onVote = (choice) =>
if (new Date().getTime() > Number(proposal.duration + '000'))
toast.warning('Proposal expired!')
return
voteOnProposal(id, choice).then((res) =>
if (!!!res.code)
toast.success('Voted successfully!')
window.location.reload()
)
const daysRemaining = (days) =>
const todaysdate = moment()
days = Number((days + '000').slice(0))
days = moment(days).format('YYYY-MM-DD')
days = moment(days)
days = days.diff(todaysdate, 'days')
return days == 1 ? '1 day' : days + ' days'
const onEnterChat = () =>
if (group.hasJoined)
navigator(`/chat/$`pid_$id``)
else
joinGroup(`pid_$id`).then((res) =>
if (!!res)
navigator(`/chat/$`pid_$id``)
console.log('Success joining: ', res)
else
console.log('Error Joining Group: ', res)
)
const onCreateGroup = () =>
createNewGroup(`pid_$id`, proposal.title).then((group) =>
if (!!!group.code)
toast.success('Group created successfully!')
setGroup(group)
else
console.log('Error Creating Group: ', group)
)
return (
<div className="p-8">
<h2 className="font-semibold text-3xl mb-5">proposal?.title</h2>
<p>
This proposal is to payout <strong>proposal?.amount Eth</strong> and
currently have' '
<strong>proposal?.upvotes + proposal?.downvotes votes</strong> and
will expire in <strong>daysRemaining(proposal?.duration)</strong>
</p>
<hr className="my-6 border-gray-300" />
<p>proposal?.description</p>
<div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto">
<BarChart width=730 height=250 data=data>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="Acceptees" fill="#2563eb" />
<Bar dataKey="Rejectees" fill="#dc2626" />
</BarChart>
</div>
<div
className="flex flex-row justify-start items-center space-x-3 mt-4"
role="group"
>
isStakeholder ? (
<>
<button
type="button"
className="inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded-full shadow-md
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out dark:text-gray-300
dark:border dark:border-gray-500 dark:bg-transparent"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=() => onVote(true)
>
Accept
</button>
<button
type="button"
className="inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded-full shadow-md
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out
dark:border dark:border-gray-500 dark:bg-transparent"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=() => onVote(false)
>
Reject
</button>
currentUser &&
currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() &&
!group ? (
<button
type="button"
className="inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded-full shadow-md
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out
dark:border dark:border-blue-500"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=onCreateGroup
>
Create Group
</button>
) : null
</>
) : null
currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? (
<button
type="button"
className="inline-block px-6 py-2.5
bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded-full shadow-md
hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:bg-blue-800 active:shadow-lg transition
duration-150 ease-in-out
dark:border dark:border-blue-500"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
onClick=onEnterChat
>
Chat
</button>
) : null
proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() &&
!!!group ? (
<button
type="button"
className="inline-block px-6 py-2.5 bg-blue-600
dark:bg-transparent text-white font-medium text-xs
leading-tight uppercase rounded-full shadow-md
hover:border-blue-700 hover:shadow-lg focus:border-blue-700
focus:shadow-lg focus:outline-none focus:ring-0
active:border-blue-800 active:shadow-lg transition
duration-150 ease-in-out dark:text-blue-500
dark:border dark:border-blue-500 disabled:bg-blue-300"
data-mdb-ripple="true"
data-mdb-ripple-color="light"
disabled
>
Group N/A
</button>
) : null
</div>
</div>
)
export default ProposalDetails
8.5 DAO应用选民组件
该组件仅列出对提案进行投票的利益相关者。该组件还为用户提供了在拒绝者和接受者之间进行过滤的机会。请参阅下面的代码。
import Identicon from 'react-identicons'
import moment from 'moment'
import useState, useEffect from 'react'
import useParams from 'react-router-dom'
import truncate from '../store'
import listVoters from '../Dominion'
const Voters = () =>
const [voters, setVoters] = useState([])
const [data, setData] = useState([])
const id = useParams()
const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow()
const deactive = `bg-transparent
text-blue-600 font-medium text-xs leading-tight
uppercase hover:bg-blue-700 focus:bg-blue-700
focus:outline-none focus:ring-0 active:bg-blue-600
transition duration-150 ease-in-out overflow-hidden
border border-blue-600 hover:text-white focus:text-white`
const active = `bg-blue-600
text-white font-medium text-xs leading-tight
uppercase hover:bg-blue-700 focus:bg-blue-700
focus:outline-none focus:ring-0 active:bg-blue-800
transition duration-150 ease-in-out overflow-hidden
border border-blue-600`
useEffect(() =>
listVoters(id).then((res) =>
setVoters(res)
setData(res)
)
, [id])
const getAll = () => setVoters(data)
const getAccepted = () => setVoters(data.filter((vote) => vote.choosen))
const getRejected = () => setVoters(data.filter((vote) => !vote.choosen))
return (
<div className="flex flex-col p-8">
<div className="flex flex-row justify-center items-center" role="group">
<button
aria-current="page"
className=`rounded-l-full px-6 py-2.5 $active`
onClick=getAll
>
All
</button>
<button
aria-current="page"
className=`px-6 py-2.5 $deactive`
onClick=getAccepted
>
Acceptees
</button>
<button
aria-current="page"
className=`rounded-r-full px-6 py-2.5 $deactive`
onClick=getRejected
>
Rejectees
</button>
</div>
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
<div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md">
<table className="min-w-full">
<thead className="border-b dark:border-gray-500">
<tr>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Voter
</th>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Voted
</th>
<th
scope="col"
className="text-sm font-medium px-6 py-4 text-left"
>
Vote
</th>
</tr>
</thead>
<tbody>
voters.map((voter, i) => (
<tr
key=i
className="border-b dark:border-gray-500 transition duration-300 ease-in-out"
>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
<div className="flex flex-row justify-start items-center space-x-3">
<Identicon
string=voter.voter.toLowerCase()
size=25
className="h-10 w-10 object-contain rounded-full mr-3"
/>
<span>truncate(voter.voter, 4, 4, 11)</span>
</div>
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
timeAgo(voter.timestamp)
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
voter.choosen ? (
<button
className以上是关于DAO开发教程WEB3.0的主要内容,如果未能解决你的问题,请参考以下文章