什么是 DAO?
DAO代表去中心化的自治组织。您可以将 DAO 视为类似于现实世界中的公司。从本质上讲,DAO 允许成员创建治理决策并对其进行投票。
在传统公司中,当需要做出决定时,公司的董事会或高管负责做出该决定。然而,在 DAO 中,这个过程是民主化的,任何成员都可以创建提案,所有其他成员都可以对其进行投票。创建的每个提案都有一个投票截止日期,在截止日期之后做出有利于投票结果的决定(是或否)。
DAO 的成员资格通常受到 ERC20 Token 所有权或 NFT 所有权的限制。成员资格和投票权与您拥有的Token数量成正比的 DAO 示例包括Uniswap和ENS。基于 NFT 的 DAO 示例包括Meebits DAO。
构建我们的 DAO
假如你想为你的CryptoDevs
NFT 持有者启动一个 DAO。从通过 ICO 获得的 ETH 中,你建立了一个 DAO 库。DAO 现在有很多 ETH,但目前什么也没做。
您希望允许您的 NFT 持有者创建并投票使用该 ETH 从 NFT 市场购买其他 NFT 的提案,并推测价格。也许将来当你卖回 NFT 时,你会将利润分配给 DAO 的所有成员。
要求
- 任何拥有
CryptoDevs
NFT 的人都可以创建从 NFT 市场购买不同 NFT 的提案
- 每个拥有
CryptoDevs
NFT 的人都可以投票支持或反对活跃的提案
- 每个 NFT 计为每个提案的一票
- 投票者不能对具有相同 NFT 的同一个提案多次投票
- 如果在截止日期前多数选民投票支持该提案,NFT 购买将自动执行
我们将做什么
- 为了能够在提案通过时自动购买 NFT,您需要一个可以调用
purchase()
函数的链上 NFT 市场。那里有很多 NFT 市场,但为了避免事情过于复杂,我们将为本教程创- 建一个简化的假 NFT 市场,因为重点是 DAO。
- 我们还将使用 Hardhat 制作实际的 DAO 智能合约。
- 我们将使用 Next.js 制作网站,以允许用户创建和对提案进行投票
先决条件
构建
智能合约开发
我们将从创建智能合约开始。我们将制作两个智能合约:
FakeNFTMarketplace.sol
CryptoDevsDAO.sol
为此,我们将使用我们在过去几个教程中一直使用的Hardhat开发框架。
1
2
3
4
|
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
|
现在您已经安装了 Hardhat,我们可以设置一个项目。在终端中执行以下命令。
-
选择Create a Javascript project
-
按回车键已指定Hardhat Project root
-
如果您想添加一个问题,请按 Enter 键.gitignore
-
按回车键Do you want to install this sample project’s dependencies with npm (@nomicfoundation/hardhat-toolbox)?
现在你有一个安全帽项目准备好了!
如果您在 Windows 上,请执行此额外步骤并安装这些库:)
1
|
npm install --save-dev @nomicfoundation/hardhat-toolbox
|
并按下Enter所有问题(选择Create a basic sample project)选项。
- 现在,让我们从 NPM 安装包
@openzeppelin/contracts
开始,因为我们将使用OpenZeppelin 的 Ownable Contract作为 DAO 合约。
1
|
npm install @openzeppelin/contracts
|
- 首先,让我们做一个简单的
Fake NFT Marketplace
。在hardhat-tutorial
contracts
的目录下创建一个名为FakeNFTMarketplace.sol
的文件,并添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FakeNFTMarketplace {
/// @dev Maintain a mapping of Fake TokenID to Owner addresses
mapping(uint256 => address) public tokens;
/// @dev Set the purchase price for each Fake NFT
uint256 NFTPrice = 0.1 ether;
/// @dev purchase() accepts ETH and marks the owner of the given tokenId as the caller address
/// @param _tokenId - the fake NFT token Id to purchase
function purchase(uint256 _tokenId) external payable {
require(msg.value == NFTPrice, "This NFT costs 0.1 ether");
tokens[_tokenId] = msg.sender;
}
/// @dev getPrice() returns the price of one NFT
function getPrice() external view returns (uint256) {
return NFTPrice;
}
/// @dev available() checks whether the given tokenId has already been sold or not
/// @param _tokenId - the tokenId to check for
function available(uint256 _tokenId) external view returns (bool) {
// address(0) = 0x0000000000000000000000000000000000000000
// This is the default value for addresses in Solidity
if (tokens[_tokenId] == address(0)) {
return true;
}
return false;
}
}
|
并确保没有编译错误。
1
2
3
4
5
6
7
8
9
10
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
// We will add the Interfaces here
contract CryptoDevsDAO is Ownable {
// We will write contract code here
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/**
* Interface for the FakeNFTMarketplace
*/
interface IFakeNFTMarketplace {
/// @dev getPrice() returns the price of an NFT from the FakeNFTMarketplace
/// @return Returns the price in Wei for an NFT
function getPrice() external view returns (uint256);
/// @dev available() returns whether or not the given _tokenId has already been purchased
/// @return Returns a boolean value - true if available, false if not
function available(uint256 _tokenId) external view returns (bool);
/// @dev purchase() purchases an NFT from the FakeNFTMarketplace
/// @param _tokenId - the fake NFT tokenID to purchase
function purchase(uint256 _tokenId) external payable;
}
/**
* Minimal interface for CryptoDevsNFT containing only two functions
* that we are interested in
*/
interface ICryptoDevsNFT {
/// @dev balanceOf returns the number of NFTs owned by the given address
/// @param owner - address to fetch number of NFTs for
/// @return Returns the number of NFTs owned
function balanceOf(address owner) external view returns (uint256);
/// @dev tokenOfOwnerByIndex returns a tokenID at given index for owner
/// @param owner - address to fetch the NFT TokenID for
/// @param index - index of NFT in owned tokens array to fetch
/// @return Returns the TokenID of the NFT
function tokenOfOwnerByIndex(address owner, uint256 index)
external
view
returns (uint256);
}
|
-
现在,让我们考虑一下我们在 DAO 合约中需要哪些功能。
-
以合约状态存储已创建的提案
-
允许 CryptoDevs NFT 的持有者创建新提案
-
允许 CryptoDevs NFT 的持有者对提案进行投票,因为他们尚未投票,并且提案尚未通过其截止日期
-
允许 CryptoDevs NFT 的持有者在超过截止日期后执行提案,以在提案通过时触发 NFT 购买
-
让我们首先创建一个代表Proposal的结构。在你的合约中,添加以下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Create a struct named Proposal containing all relevant information
struct Proposal {
// NFTTokenId - the tokenID of the NFT to purchase from FakeNFTMarketplace if the proposal passes
uint256 NFTTokenId;
// deadline - the UNIX timestamp until which this proposal is active. Proposal can be executed after the deadline has been exceeded.
uint256 deadline;
// yayVotes - number of yay votes for this proposal
uint256 yayVotes;
// nayVotes - number of nay votes for this proposal
uint256 nayVotes;
// executed - whether or not this proposal has been executed yet. Cannot be executed before the deadline has been exceeded.
bool executed;
// voters - a mapping of CryptoDevsNFT tokenIDs to booleans indicating whether that NFT has already been used to cast a vote or not
mapping(uint256 => bool) voters;
}
|
- 让我们也创建一个从提案ID到提案的映射,以保存所有创建的提案,以及一个计数器来计算存在的提案数量。
1
2
3
4
|
// Create a mapping of ID to Proposal
mapping(uint256 => Proposal) public proposals;
// Number of proposals that have been created
uint256 public numProposals;
|
- 现在,由于我们将在
FakeNFTMarketplace
和CryptoDevsNFT
合约上调用函数,让我们为这些合约初始化变量。
1
2
|
IFakeNFTMarketplace NFTMarketplace;
ICryptoDevsNFT cryptoDevsNFT;
|
- 创建一个构造函数,初始化这些合约变量,同时接受部署者的ETH存款以填充DAO的ETH库。(在后台,由于我们导入了
Ownable
合约,这也将设置合约部署者为该合约的所有者)。
1
2
3
4
5
6
7
|
// Create a payable constructor which initializes the contract
// instances for FakeNFTMarketplace and CryptoDevsNFT
// The payable allows this constructor to accept an ETH deposit when it is being deployed
constructor(address _NFTMarketplace, address _cryptoDevsNFT) payable {
NFTMarketplace = IFakeNFTMarketplace(_NFTMarketplace);
cryptoDevsNFT = ICryptoDevsNFT(_cryptoDevsNFT);
}
|
- 现在,由于我们希望几乎所有的其他函数只被那些拥有
CryptoDevs NFT
合约的人调用,我们将创建一个modifier
以避免重复代码。
1
2
3
4
5
6
|
// Create a modifier which only allows a function to be
// called by someone who owns at least 1 CryptoDevsNFT
modifier NFTHolderOnly() {
require(cryptoDevsNFT.balanceOf(msg.sender) > 0, "NOT_A_DAO_MEMBER");
_;
}
|
- 我们现在有足够的能力来编写我们的
createProposal
函数,它将允许成员创建新的提案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/// @dev createProposal allows a CryptoDevsNFT holder to create a new proposal in the DAO
/// @param _NFTTokenId - the tokenID of the NFT to be purchased from FakeNFTMarketplace if this proposal passes
/// @return Returns the proposal index for the newly created proposal
function createProposal(uint256 _NFTTokenId)
external
NFTHolderOnly
returns (uint256)
{
require(NFTMarketplace.available(_NFTTokenId), "NFT_NOT_FOR_SALE");
Proposal storage proposal = proposals[numProposals];
proposal.NFTTokenId = _NFTTokenId;
// Set the proposal's voting deadline to be (current time + 5 minutes)
proposal.deadline = block.timestamp + 5 minutes;
numProposals++;
return numProposals - 1;
}
|
- 现在,要对一个提案进行投票,我们要增加一个额外的限制,即被投票的提案必须没有超过其最后期限。为了做到这一点,我们将创建第二个修改器。
1
2
3
4
5
6
7
8
9
|
// Create a modifier which only allows a function to be
// called if the given proposal's deadline has not been exceeded yet
modifier activeProposalOnly(uint256 proposalIndex) {
require(
proposals[proposalIndex].deadline > block.timestamp,
"DEADLINE_EXCEEDED"
);
_;
}
|
注意这个修改器是如何接受一个参数的!
- 此外,由于投票只能是两个值中的一个(YAY或NAY)–我们可以创建一个代表可能选项的枚举。
1
2
3
4
5
|
// Create an enum named Vote containing possible options for a vote
enum Vote {
YAY, // YAY = 0
NAY // NAY = 1
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/// @dev voteOnProposal allows a CryptoDevsNFT holder to cast their vote on an active proposal
/// @param proposalIndex - the index of the proposal to vote on in the proposals array
/// @param vote - the type of vote they want to cast
function voteOnProposal(uint256 proposalIndex, Vote vote)
external
NFTHolderOnly
activeProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];
uint256 voterNFTBalance = cryptoDevsNFT.balanceOf(msg.sender);
uint256 numVotes = 0;
// Calculate how many NFTs are owned by the voter
// that haven't already been used for voting on this proposal
for (uint256 i = 0; i < voterNFTBalance; i++) {
uint256 tokenId = cryptoDevsNFT.tokenOfOwnerByIndex(msg.sender, i);
if (proposal.voters[tokenId] == false) {
numVotes++;
proposal.voters[tokenId] = true;
}
}
require(numVotes > 0, "ALREADY_VOTED");
if (vote == Vote.YAY) {
proposal.yayVotes += numVotes;
} else {
proposal.nayVotes += numVotes;
}
}
|
- 我们快完成了!为了执行一个已经超过截止日期的提案,我们将创建最后的修改器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// Create a modifier which only allows a function to be
// called if the given proposals' deadline HAS been exceeded
// and if the proposal has not yet been executed
modifier inactiveProposalOnly(uint256 proposalIndex) {
require(
proposals[proposalIndex].deadline <= block.timestamp,
"DEADLINE_NOT_EXCEEDED"
);
require(
proposals[proposalIndex].executed == false,
"PROPOSAL_ALREADY_EXECUTED"
);
_;
}
|
注意这个修改器也需要一个参数!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/// @dev executeProposal allows any CryptoDevsNFT holder to execute a proposal after it's deadline has been exceeded
/// @param proposalIndex - the index of the proposal to execute in the proposals array
function executeProposal(uint256 proposalIndex)
external
NFTHolderOnly
inactiveProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];
// If the proposal has more YAY votes than NAY votes
// purchase the NFT from the FakeNFTMarketplace
if (proposal.yayVotes > proposal.nayVotes) {
uint256 NFTPrice = NFTMarketplace.getPrice();
require(address(this).balance >= NFTPrice, "NOT_ENOUGH_FUNDS");
NFTMarketplace.purchase{value: NFTPrice}(proposal.NFTTokenId);
}
proposal.executed = true;
}
|
1
2
3
4
|
/// @dev withdrawEther allows the contract owner (deployer) to withdraw the ETH from the contract
function withdrawEther() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
|
这将把合约的全部ETH余额转移到业主地址
- 最后,为了允许向DAO金库增加更多的ETH存款,我们需要增加一些特殊的功能。通常情况下,合约地址不能接受发给他们的ETH,除非是通过
payable
函数。但是我们不希望用户只是为了存钱而调用函数,他们应该能够直接从他们的钱包里转出ETH。为此,让我们添加这两个函数。
1
2
3
4
5
|
// The following two functions allow the contract to accept ETH deposits
// directly from a wallet without calling a function
receive() external payable {}
fallback() external payable {}
|
智能合约的部署
现在我们已经写好了合约,让我们把它们部署到Rinkeby Testnet上。确保你在Rinkeby Testnet上有一些ETH。
- 安装NPM的
dotenv
包,以便能够使用hardhat.config.js
中.env文件中指定的环境变量。在hardhat-tutorial
目录下的终端中执行以下命令。
- 现在在
hardhat-tutorial
目录下创建一个.env
文件,设置以下两个环境变量。按照说明来获取它们的值。确保你使用的Rinkeby私钥在Rinkeby Testnet上有ETH。
1
2
3
4
5
6
7
8
9
|
// Go to https://www.alchemyapi.io, sign up, create
// a new App in its dashboard and select the network as Rinkeby, and replace "add-the-alchemy-key-url-here" with its key url
ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here"
// Replace this private key with your RINKEBY account private key
// To export your private key from Metamask, open Metamask and
// go to Account Details > Export Private Key
// Be aware of NEVER putting real Ether into testing accounts
RINKEBY_PRIVATE_KEY="add-the-rinkeby-private-key-here"
|
- 现在,让我们写一个部署脚本,为我们自动部署我们的两个合约。在
hardhat-tutorial/scripts
下创建一个新文件,或替换现有的默认文件,命名为deploy.js
,并添加以下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
const { ethers } = require("hardhat");
const { CRYPTODEVS_NFT_CONTRACT_ADDRESS } = require("../constants");
async function main() {
// Deploy the FakeNFTMarketplace contract first
const FakeNFTMarketplace = await ethers.getContractFactory(
"FakeNFTMarketplace"
);
const fakeNFTMarketplace = await FakeNFTMarketplace.deploy();
await fakeNFTMarketplace.deployed();
console.log("FakeNFTMarketplace deployed to: ", fakeNFTMarketplace.address);
// Now deploy the CryptoDevsDAO contract
const CryptoDevsDAO = await ethers.getContractFactory("CryptoDevsDAO");
const cryptoDevsDAO = await CryptoDevsDAO.deploy(
fakeNFTMarketplace.address,
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
{
// This assumes your account has at least 1 ETH in it's account
// Change this value as you want
value: ethers.utils.parseEther("1"),
}
);
await cryptoDevsDAO.deployed();
console.log("CryptoDevsDAO deployed to: ", cryptoDevsDAO.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
|
- 你可能已经注意到,
deploy.js
从一个名为constants
的文件中导入了一个名为CRYPTODEVS_NFT_CONTRACT_ADDRESS
的变量。让我们来做这个。在hardhat-tutorial
目录下创建一个名为constants.js
的新文件。
1
2
3
4
5
|
// Replace the value with your NFT contract address
const CRYPTODEVS_NFT_CONTRACT_ADDRESS =
"YOUR_CRYPTODEVS_NFT_CONTRACT_ADDRESS_HERE";
module.exports = { CRYPTODEVS_NFT_CONTRACT_ADDRESS };
|
- 现在,让我们把Rinkeby网络添加到你的Hardhat配置中,以便我们可以部署到Rinkeby。打开你的
hardhat.config.js
文件,用以下内容替换它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });
const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;
const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY;
module.exports = {
solidity: "0.8.9",
networks: {
rinkeby: {
url: ALCHEMY_API_KEY_URL,
accounts: [RINKEBY_PRIVATE_KEY],
},
},
};
|
- 让我们在继续进行之前确保一切都能编译。在
hardhat-tutorial
文件夹中从终端执行以下命令。
并确保没有编译错误。如果你确实遇到了编译错误,试着将你的代码与这里的最终版本进行比较
- 让我们开始部署吧!在终端中从
hardhat-tutorial
目录中执行以下命令
1
|
npx hardhat run scripts/deploy.js --network rinkeby
|
- 保存
FakeNFTMarketplace
和CryptoDevsDAO
的合约地址,它们被打印在你的终端上。你以后会需要这些。
前端开发
哇!已经开发了这么多!
我们已经成功开发并部署了我们的合约到 Rinkeby 测试网。现在,是时候构建前端界面了,这样用户就可以从网站上创建提案并对其进行投票。
为了开发网站,我们将使用目前为止的Next.js,这是一个建立在React之上的元框架。
- 让我们从创建一个新
next
应用开始。设置next
应用程序后,您的文件夹结构应如下所示:
1
2
3
|
- DAO-Tutorial
- hardhat-tutorial
- my-app
|
- 要创建,请在目录
my-app
中的终端中执行以下命令DAO-Tutorial
1
|
npx create-next-app@latest
|
并按下Enter所有问题提示。这应该创建my-app文件夹并设置一个基本的 Next.js 项目。
1
2
|
cd my-app
npm run dev
|
1
|
npm install web3modal ethers
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
.main {
min-height: 90vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: "Courier New", Courier, monospace;
}
.footer {
display: flex;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.image {
width: 70%;
height: 50%;
margin-left: 20%;
}
.title {
font-size: 2rem;
margin: 2rem 0;
}
.description {
line-height: 1;
margin: 2rem 0;
font-size: 1.2rem;
}
.button {
border-radius: 4px;
background-color: blue;
border: none;
color: #ffffff;
font-size: 15px;
padding: 10px;
width: 200px;
cursor: pointer;
margin-right: 2%;
}
.button2 {
border-radius: 4px;
background-color: indigo;
border: none;
color: #ffffff;
font-size: 15px;
padding: 10px;
cursor: pointer;
margin-right: 2%;
margin-top: 1rem;
}
.proposalCard {
width: fit-content;
margin-top: 0.25rem;
border: black 2px solid;
flex: 1;
flex-direction: column;
}
.container {
margin-top: 2rem;
}
.flex {
flex: 1;
justify-content: space-between;
}
@media (max-width: 1000px) {
.main {
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
|
- 该网站还需要从两个智能合约–
CryptoDevsDAO
和CryptoDevsNFT
中读/写数据。让我们把它们的合约地址和ABI存储在一个常量文件中。在my-app
目录下创建一个constants.js
文件。
1
2
3
4
5
|
export const CRYPTODEVS_DAO_CONTRACT_ADDRESS = "";
export const CRYPTODEVS_NFT_CONTRACT_ADDRESS = "";
export const CRYPTODEVS_DAO_ABI = [];
export const CRYPTODEVS_NFT_ABI = [];
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
|
import { Contract, providers } from "ethers";
import { formatEther } from "ethers/lib/utils";
import Head from "next/head";
import { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import {
CRYPTODEVS_DAO_ABI,
CRYPTODEVS_DAO_CONTRACT_ADDRESS,
CRYPTODEVS_NFT_ABI,
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
} from "../constants";
import styles from "../styles/Home.module.css";
export default function Home() {
// ETH Balance of the DAO contract
const [treasuryBalance, setTreasuryBalance] = useState("0");
// Number of proposals created in the DAO
const [numProposals, setNumProposals] = useState("0");
// Array of all proposals created in the DAO
const [proposals, setProposals] = useState([]);
// User's balance of CryptoDevs NFTs
const [NFTBalance, setNFTBalance] = useState(0);
// Fake NFT Token ID to purchase. Used when creating a proposal.
const [fakeNFTTokenId, setFakeNFTTokenId] = useState("");
// One of "Create Proposal" or "View Proposals"
const [selectedTab, setSelectedTab] = useState("");
// True if waiting for a transaction to be mined, false otherwise.
const [loading, setLoading] = useState(false);
// True if user has connected their wallet, false otherwise
const [walletConnected, setWalletConnected] = useState(false);
const web3ModalRef = useRef();
// Helper function to connect wallet
const connectWallet = async () => {
try {
await getProviderOrSigner();
setWalletConnected(true);
} catch (error) {
console.error(error);
}
};
// Reads the ETH balance of the DAO contract and sets the `treasuryBalance` state variable
const getDAOTreasuryBalance = async () => {
try {
const provider = await getProviderOrSigner();
const balance = await provider.getBalance(
CRYPTODEVS_DAO_CONTRACT_ADDRESS
);
setTreasuryBalance(balance.toString());
} catch (error) {
console.error(error);
}
};
// Reads the number of proposals in the DAO contract and sets the `numProposals` state variable
const getNumProposalsInDAO = async () => {
try {
const provider = await getProviderOrSigner();
const contract = getDaoContractInstance(provider);
const daoNumProposals = await contract.numProposals();
setNumProposals(daoNumProposals.toString());
} catch (error) {
console.error(error);
}
};
// Reads the balance of the user's CryptoDevs NFTs and sets the `NFTBalance` state variable
const getUserNFTBalance = async () => {
try {
const signer = await getProviderOrSigner(true);
const NFTContract = getCryptodevsNFTContractInstance(signer);
const balance = await NFTContract.balanceOf(signer.getAddress());
setNFTBalance(parseInt(balance.toString()));
} catch (error) {
console.error(error);
}
};
// Calls the `createProposal` function in the contract, using the tokenId from `fakeNFTTokenId`
const createProposal = async () => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);
const txn = await daoContract.createProposal(fakeNFTTokenId);
setLoading(true);
await txn.wait();
await getNumProposalsInDAO();
setLoading(false);
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};
// Helper function to fetch and parse one proposal from the DAO contract
// Given the Proposal ID
// and converts the returned data into a Javascript object with values we can use
const fetchProposalById = async (id) => {
try {
const provider = await getProviderOrSigner();
const daoContract = getDaoContractInstance(provider);
const proposal = await daoContract.proposals(id);
const parsedProposal = {
proposalId: id,
NFTTokenId: proposal.NFTTokenId.toString(),
deadline: new Date(parseInt(proposal.deadline.toString()) * 1000),
yayVotes: proposal.yayVotes.toString(),
nayVotes: proposal.nayVotes.toString(),
executed: proposal.executed,
};
return parsedProposal;
} catch (error) {
console.error(error);
}
};
// Runs a loop `numProposals` times to fetch all proposals in the DAO
// and sets the `proposals` state variable
const fetchAllProposals = async () => {
try {
const proposals = [];
for (let i = 0; i < numProposals; i++) {
const proposal = await fetchProposalById(i);
proposals.push(proposal);
}
setProposals(proposals);
return proposals;
} catch (error) {
console.error(error);
}
};
// Calls the `voteOnProposal` function in the contract, using the passed
// proposal ID and Vote
const voteOnProposal = async (proposalId, _vote) => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);
let vote = _vote === "YAY" ? 0 : 1;
const txn = await daoContract.voteOnProposal(proposalId, vote);
setLoading(true);
await txn.wait();
setLoading(false);
await fetchAllProposals();
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};
// Calls the `executeProposal` function in the contract, using
// the passed proposal ID
const executeProposal = async (proposalId) => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);
const txn = await daoContract.executeProposal(proposalId);
setLoading(true);
await txn.wait();
setLoading(false);
await fetchAllProposals();
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};
// Helper function to fetch a Provider/Signer instance from Metamask
const getProviderOrSigner = async (needSigner = false) => {
const provider = await web3ModalRef.current.connect();
const web3Provider = new providers.Web3Provider(provider);
const { chainId } = await web3Provider.getNetwork();
if (chainId !== 4) {
window.alert("Please switch to the Rinkeby network!");
throw new Error("Please switch to the Rinkeby network");
}
if (needSigner) {
const signer = web3Provider.getSigner();
return signer;
}
return web3Provider;
};
// Helper function to return a DAO Contract instance
// given a Provider/Signer
const getDaoContractInstance = (providerOrSigner) => {
return new Contract(
CRYPTODEVS_DAO_CONTRACT_ADDRESS,
CRYPTODEVS_DAO_ABI,
providerOrSigner
);
};
// Helper function to return a CryptoDevs NFT Contract instance
// given a Provider/Signer
const getCryptodevsNFTContractInstance = (providerOrSigner) => {
return new Contract(
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
CRYPTODEVS_NFT_ABI,
providerOrSigner
);
};
// piece of code that runs everytime the value of `walletConnected` changes
// so when a wallet connects or disconnects
// Prompts user to connect wallet if not connected
// and then calls helper functions to fetch the
// DAO Treasury Balance, User NFT Balance, and Number of Proposals in the DAO
useEffect(() => {
if (!walletConnected) {
web3ModalRef.current = new Web3Modal({
network: "rinkeby",
providerOptions: {},
disableInjectedProvider: false,
});
connectWallet().then(() => {
getDAOTreasuryBalance();
getUserNFTBalance();
getNumProposalsInDAO();
});
}
}, [walletConnected]);
// Piece of code that runs everytime the value of `selectedTab` changes
// Used to re-fetch all proposals in the DAO when user switches
// to the 'View Proposals' tab
useEffect(() => {
if (selectedTab === "View Proposals") {
fetchAllProposals();
}
}, [selectedTab]);
// Render the contents of the appropriate tab based on `selectedTab`
function renderTabs() {
if (selectedTab === "Create Proposal") {
return renderCreateProposalTab();
} else if (selectedTab === "View Proposals") {
return renderViewProposalsTab();
}
return null;
}
// Renders the 'Create Proposal' tab content
function renderCreateProposalTab() {
if (loading) {
return (
<div className={styles.description}>
Loading... Waiting for transaction...
</div>
);
} else if (NFTBalance === 0) {
return (
<div className={styles.description}>
You do not own any CryptoDevs NFTs. <br />
<b>You cannot create or vote on proposals</b>
</div>
);
} else {
return (
<div className={styles.container}>
<label>Fake NFT Token ID to Purchase: </label>
<input
placeholder="0"
type="number"
onChange={(e) => setFakeNFTTokenId(e.target.value)}
/>
<button className={styles.button2} onClick={createProposal}>
Create
</button>
</div>
);
}
}
// Renders the 'View Proposals' tab content
function renderViewProposalsTab() {
if (loading) {
return (
<div className={styles.description}>
Loading... Waiting for transaction...
</div>
);
} else if (proposals.length === 0) {
return (
<div className={styles.description}>
No proposals have been created
</div>
);
} else {
return (
<div>
{proposals.map((p, index) => (
<div key={index} className={styles.proposalCard}>
<p>Proposal ID: {p.proposalId}</p>
<p>Fake NFT to Purchase: {p.NFTTokenId}</p>
<p>Deadline: {p.deadline.toLocaleString()}</p>
<p>Yay Votes: {p.yayVotes}</p>
<p>Nay Votes: {p.nayVotes}</p>
<p>Executed?: {p.executed.toString()}</p>
{p.deadline.getTime() > Date.now() && !p.executed ? (
<div className={styles.flex}>
<button
className={styles.button2}
onClick={() => voteOnProposal(p.proposalId, "YAY")}
>
Vote YAY
</button>
<button
className={styles.button2}
onClick={() => voteOnProposal(p.proposalId, "NAY")}
>
Vote NAY
</button>
</div>
) : p.deadline.getTime() < Date.now() && !p.executed ? (
<div className={styles.flex}>
<button
className={styles.button2}
onClick={() => executeProposal(p.proposalId)}
>
Execute Proposal{" "}
{p.yayVotes > p.nayVotes ? "(YAY)" : "(NAY)"}
</button>
</div>
) : (
<div className={styles.description}>Proposal Executed</div>
)}
</div>
))}
</div>
);
}
}
return (
<div>
<Head>
<title>CryptoDevs DAO</title>
<meta name="description" content="CryptoDevs DAO" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.main}>
<div>
<h1 className={styles.title}>Welcome to Crypto Devs!</h1>
<div className={styles.description}>Welcome to the DAO!</div>
<div className={styles.description}>
Your CryptoDevs NFT Balance: {NFTBalance}
<br />
Treasury Balance: {formatEther(treasuryBalance)} ETH
<br />
Total Number of Proposals: {numProposals}
</div>
<div className={styles.flex}>
<button
className={styles.button}
onClick={() => setSelectedTab("Create Proposal")}
>
Create Proposal
</button>
<button
className={styles.button}
onClick={() => setSelectedTab("View Proposals")}
>
View Proposals
</button>
</div>
{renderTabs()}
</div>
<div>
<img className={styles.image} src="/cryptodevs/0.svg" />
</div>
</div>
<footer className={styles.footer}>
Made with ❤ by Crypto Devs
</footer>
</div>
);
}
|
- 让我们来运行它!在你的终端,从
my-app
目录中,执行。
来看看你的网站的运行情况。它应该看起来像本教程开头的截图。
祝贺您!您的CryptoDevs DAO网站现在应该可以运行了。你的CryptoDevs DAO网站现在应该工作了。
测试
- 创建几个提案
- YAY尝试对一个和NAY另一个投票
- 等待 5 分钟让他们的最后期限过去
- 执行这两个。
- 观察 DAO 财政部的余额
0.1 ETH
由于在执行时购买 NFT 时通过的提案而下降。
推送到 Github
在继续下一步之前,请确保将所有这些代码推送到 Github。
网站部署
如果您无法与他人共享网站,那么它有什么用?让我们努力将您的 dApp 部署到世界各地,以便您可以与所有 LearnWeb3DAO 朋友分享它。
- 转到Vercel 仪表板并使用您的 GitHub 帐户登录。
- 单击New Project按钮并选择您的DAO-Tutorial存储库。
- 在配置您的新项目时,Vercel 将允许您自定义您的Root Directory
- 由于我们的 Next.js 应用程序位于 repo 的子文件夹中,因此我们需要对其进行修改。
- 单击Edit旁边Root Directory并将其设置为my-app。
- 选择框架为Next.js
- 点击Deploy
- 现在,您可以通过转到 Vercel 仪表板、选择您的项目并从那里复制域来查看您部署的网站!
恭喜!你们都完成了!
原文:https://www.learnweb3.io/tracks/sophomore/decentralized-autonomous-organizations