这 ”
我最初文章的最终结果展示了房主协会 (HOA) 如何使用 Web3 技术来主持他们的选举投票。原始设计的问题在于底层智能合约只允许一个是或否的答案。这是为了让智能合约保持简单,同时引入使用 Web3 技术创建 HOA 选票所需的其他概念。
本出版物的目的是深入研究智能合约,以构建一个应用程序,该应用程序不仅可以捕获 HOA 投票的实际需求和功能,而且可以设计一个可以在一次选举到下一次选举中重复使用的应用程序。
在开始之前,让我们定义一个智能合约:
“智能合约是在以太坊地址上运行的程序。它们由可以在收到交易时执行的数据和函数组成。以下是构成智能合约的概述。”
信不信由你,智能合约的一个简单示例可以在一个简单的口香糖机中找到:
人们很容易理解与使用口香糖机购买相关的成本。通常,这是一个(美国)季度。在这里需要指出的是,顾客是匿名的,因为口香糖机器不需要在给他们一块美味口香糖之前知道一个人是谁。
匿名消费者将货币放入口香糖机并转动表盘以接受合同条款。这一步很重要,因为交易是透明的和点对点的:在你和机器之间。交易也是安全的,因为您必须提供预期的货币才能使用口香糖机。
一旦货币落入口香糖机内,合同条款就被接受,口香糖球就会滚向机器底部,让顾客收到他们的购买。至此,合约已完全执行。
客户必须接受所提供的东西,这意味着他们不能退回口香糖或反转表盘来取回他们的货币。同样,智能合约通常是不可逆转和不可修改的。
除了财务驱动的示例之外,一些不可逆和不可修改的匿名、去信任、去中心化和透明交互可以实现的场景如下所示:
在任何情况下,都可以尽可能频繁地调用和审查智能合约的内容,而不能更改或修改结果。上面的每个用例都提供智能合约作为基础信息的记录系统。
目前,智能合约不是具有法律约束力的协议,除了少数例外。这意味着,如果您对智能合约的结果不满意,则不可能将您的问题提交给某些法院系统的法官。
有一些例外情况,例如在亚利桑那州,智能合约被认为具有法律约束力。此外,如果您在加利福尼亚州并且您的结婚证包含在智能合同中,则该协议也具有法律约束力。预计未来将有更多政府将智能合约视为具有法律约束力的协议。
基于“从全栈开发人员到 Web3 先锋”出版物中的简单二进制(是/否)智能合约,让我们向前迈出一步,假设对于一个有单一社区的 HOA 投票存在以下要求填补职位:
理想情况下,目标是每次进行 HOA 选举时都使用一个智能合约。那些竞选总统职位的人预计将从一次选举改变到下一次选举。
现在,让我们开始制作智能合约来满足我们的需求。
使用 Solidity,我与
// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /********************************************************/ /* For learning purposes ONLY. Do not use in production */ /********************************************************/ // Download into project folder with `npm install @openzeppelin/contracts` import "@openzeppelin/contracts/access/Ownable.sol"; // Inherits the Ownable contract so we can use its functions and modifiers contract HOABallot is Ownable { // Custom type to describe a Presidential Candidate and hold votes struct Candidate { string name; uint256 votes; } // Array of Presidential Candidates Candidate[] public candidates; // Add a President Candidate - onlyOwner function addCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "addCandidate Error: Please enter a name"); candidates.push(Candidate({name: _name, votes: 0})); } // Remove a Candidate - onlyOwner function removeCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "removeCandidate Error: Please enter a name"); bool foundCandidate = false; uint256 index; bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); // Set index number for specific candidate for (uint256 i = 0; i < candidates.length; i++) { if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { index = i; foundCandidate = true; } } // Make sure a candidate was found require(foundCandidate, "removeCandidate Error: Candidate not found"); // shift candidate to be removed to the end of the array and the rest forward for (uint256 i = index; i < candidates.length - 1; i++) { candidates[i] = candidates[i + 1]; } // remove last item from array candidates.pop(); } // Reset the President Vote Counts - onlyOwner function resetVoteCount() public onlyOwner { for (uint256 p = 0; p < candidates.length; p++) { candidates[p].votes = 0; } } // Add a vote to a candidate by name function addVoteByName(string memory _name) public { require(bytes(_name).length > 0, "addVoteByName Error: Please enter a name"); // Encode name so only need to do once bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); for (uint256 i = 0; i < candidates.length; i++) { // solidity can't compare strings directly, need to compare hash if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { candidates[i].votes += 1; } } } // Returns all the Presidential Candidates and their vote counts function getCandidates() public view returns (Candidate[] memory) { return candidates; } function getWinner() public view returns (Candidate memory winner) { uint256 winningVoteCount = 0; for (uint256 i = 0; i < candidates.length; i++) { if (candidates[i].votes > winningVoteCount) { winningVoteCount = candidates[i].votes; winner = candidates[i]; } } return winner; } }
以下是与智能合约设计相关的一些关键项目:
现在,让我们准备好使用智能合约。
为了能够使用我们的智能合约,我们将构建一个简单的 Truffle 项目并将合约部署到 Ropsten 测试网。为此,我们首先需要最新版本的 Truffle。和
npm install -g truffle
安装最新版本将使我们能够访问
接下来,创建一个新目录并初始化一个新的 Truffle 项目。
mkdir hoa-ballot-contract && cd hoa-ballot-contract truffle init
这将创建一个准系统智能合约项目,我们可以根据需要填写。所以在你最喜欢的代码编辑器中打开项目,让我们开始吧!
为了利用 OpenZeppelin,还需要在项目文件夹中执行以下命令:
npm install @openzeppelin/contracts
打开truffle-config.js文件,我们将在networks
对象中添加 Truffle Dashboard。除了所有被注释掉的样板之外,我们的对象现在应该如下所示:
networks: { dashboard: { port: 24012, } }
下一步,我们将创建一个新的智能合约文件。在contracts文件夹中,创建一个新文件并将其命名为HOABallot.sol 。从这里开始,我们将粘贴上面的智能合约。
在部署此合约之前,我们需要做的最后一件事是设置部署脚本。使用下面的内容,我们需要在名为2_hoaballot_migration.js的迁移文件夹中创建一个新文件。
const HOABallot = artifacts.require("HOABallot"); Module.exports = function (deployer) { deployer.deploy(HOABallot); }
现在我们准备将我们的合约部署到 Ropsten 测试网。在新的终端窗口中,键入以下命令以启动仪表板:
truffle dashboard
一旦它运行起来,我们的浏览器应该会弹出一个界面,要求我们连接我们的钱包。如果这没有为您弹出,请导航到localhost:24012
。
单击METAMASK按钮将通过浏览器插件启动 MetaMask。如果你没有安装钱包浏览器扩展,你可以在
输入有效密码并使用解锁按钮后,Truffle Dashboard 会确认要使用的网络:
单击CONFIRM按钮后,Truffle Dashboard 现在正在侦听请求:
我们将需要 Ropsten Eth 来进行部署。如果你没有,你可以
我们现在要做的就是部署合约。在原始终端窗口中,确保您位于项目文件夹中并键入命令:
truffle migrate --network dashboard
Truffle 将自动编译我们的智能合约,然后通过仪表板路由请求。每个请求都将遵循下面列出的相同流程。
首先,Truffle Dashboard 要求确认以处理请求:
按下PROCESS按钮后,MetaMask 插件也会要求确认:
确认按钮将允许从该关联钱包中移除资金,以处理每个请求。
该过程完成后,用于发出 truffle migrate 命令的终端窗口中将显示以下信息:
2_hoaballot_migration.js ======================== Deploying 'HOABallot' --------------------- > transaction hash: 0x5370b6f9ee1f69e92cc6289f9cb0880386f15bff389b54ab09a966c5d144f59esage. > Blocks: 0 Seconds: 32 > contract address: 0x2981d347e288E2A4040a3C17c7e5985422e3cAf2 > block number: 12479257 > block timestamp: 1656386400 > account: 0x7fC3EF335D16C0Fd4905d2C44f49b29BdC233C94 > balance: 41.088173901232893417 > gas used: 1639525 (0x190465) > gas price: 2.50000001 gwei > value sent: 0 ETH > total cost: 0.00409881251639525 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00409881251639525 ETH Summary ======= > Total deployments: 1 > Final cost: 0.00409881251639525 ETH
现在,使用合约地址值,我们可以使用以下 URL 验证智能合约:
现在我们可以切换并开始构建 Dapp。
我将使用 React CLI 创建一个名为hoa-ballot-client
的 React 应用程序:
npx create-react-app hoa-ballot-client
接下来,我将目录更改为新创建的文件夹并执行以下操作以将 web3 和 OpenZepplin 依赖项安装到 React 应用程序中:
cd hoa-ballot-client npm install web3 npm install @openzeppelin/contracts —save
根据HOABallot.sol
智能合约文件的内容,我导航到 build/contracts 文件夹并打开HOBallot.json
文件,然后将abi.js
文件的hoaBallot
常量的“abi”属性值用作如下图:
export const hoaBallot = [ { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "candidates", "outputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "removeCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "resetVoteCount", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addVoteByName", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "getCandidates", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getWinner", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate", "name": "winner", "type": "tuple" } ], "stateMutability": "view", "type": "function", "constant": true } ];
该文件被放置在 React 应用程序的 src 文件夹内新创建的 abi 文件夹中。
现在,我们需要更新 React Apps.js文件。我们先从文件顶部开始,需要配置如下图:
import React, { useState } from "react"; import { hoaBallot } from "./abi/abi"; import Web3 from "web3"; import "./App.css"; const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress);
可以通过多种方式找到contractAddress 。在本例中,我使用了 truffle — migrate CLI 命令中的结果。另一种选择是使用 Etherscan 站点。
现在,剩下的就是创建标准的 React 代码来完成以下事情:
在我的“从全栈开发人员迁移到 Web3 先锋”一文中,我也添加了 Nav 组件,以便显示投票者的地址以方便参考。
更新后的 React 应用程序现在显示如下:
const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress); const gasMultiplier = 1.5; const useStyles = makeStyles((theme) => ({ root: { "& > *": { margin: theme.spacing(1), }, }, })); const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white, fontSize: 14, fontWeight: 'bold' }, [`&.${tableCellClasses.body}`]: { fontSize: 14 }, })); function App() { const classes = useStyles(); const [newCandidateName, setNewCandidateName] = useState(""); const [account, setAccount] = useState(""); const [owner, setOwner] = useState(""); const [candidates, updateCandidates] = useState([]); const [winner, setWinner] = useState("unknown candidate"); const [waiting, setWaiting] = useState(false); const loadAccount = async(useSpinner) => { if (useSpinner) { setWaiting(true); } const web3 = new Web3(Web3.givenProvider || "http://localhost:8080"); const accounts = await web3.eth.getAccounts(); setAccount(accounts[0]); if (useSpinner) { setWaiting(false); } } const getOwner = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const owner = await storageContract.methods.owner().call(); setOwner(owner); if (useSpinner) { setWaiting(false); } }; const getCandidates = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const candidates = await storageContract.methods.getCandidates().call(); updateCandidates(candidates); await determineWinner(); if (useSpinner) { setWaiting(false); } }; const determineWinner = async () => { const winner = await storageContract.methods.getWinner().call(); if (winner && winner.name) { setWinner(winner.name); } else { setWinner("<unknown candidate>") } } const vote = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.addVoteByName(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addVoteByName(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const removeCandidate = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.removeCandidate(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.removeCandidate(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const addCandidate = async () => { setWaiting(true); const gas = (await storageContract.methods.addCandidate(newCandidateName).estimateGas({ data: newCandidateName, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addCandidate(newCandidateName).send({ from: account, data: newCandidateName, gasAsInt, }); await getCandidates(false); setWaiting(false); } React.useEffect(() => { setWaiting(true); getOwner(false).then(r => { loadAccount(false).then(r => { getCandidates(false).then(r => { setWaiting(false); }); }); }); // eslint-disable-next-line react-hooks/exhaustive-deps },[]); return ( <div className={classes.root}> <Nav /> <div className="main"> <div className="card"> <Typography variant="h3"> HOABallot </Typography> {(owner && owner.length > 0) && ( <div className="paddingBelow"> <Typography variant="caption" > This ballot is owned by: {owner} </Typography> </div> )} {waiting && ( <div className="spinnerArea" > <CircularProgress /> <Typography gutterBottom> Processing Request ... please wait </Typography> </div> )} {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && ( <div className="ownerActions generalPadding"> <Grid container spacing={3}> <Grid item xs={12}> <Typography variant="h6" gutterBottom> Ballot Owner Actions </Typography> </Grid> <Grid item xs={6} sm={6}> <TextField id="newCandidateName" value={newCandidateName} label="Candidate Name" variant="outlined" onChange={event => { const { value } = event.target; setNewCandidateName(value); }} /> </Grid> <Grid item xs={6} sm={6}> <Button id="addCandidateButton" className="button" variant="contained" color="primary" type="button" size="large" onClick={addCandidate}>Add New Candidate</Button> </Grid> </Grid> </div> )} <Typography variant="h5" gutterBottom className="generalPadding"> Candidates </Typography> {(!candidates || candidates.length === 0) && ( <div> <div className="paddingBelow"> <Typography variant="normal"> No candidates current exist. </Typography> </div> <div> <Typography variant="normal" gutterBottom> Ballot owner must use the <strong>ADD NEW CANDIDATE</strong> button to add candidates. </Typography> </div> </div> )} {(candidates && candidates.length > 0) && ( <div> <TableContainer component={Paper}> <Table sx={{ minWidth: 650 }} aria-label="customized table"> <TableHead> <TableRow> <StyledTableCell>Candidate Name</StyledTableCell> <StyledTableCell align="right">Votes</StyledTableCell> <StyledTableCell align="center">Actions</StyledTableCell> </TableRow> </TableHead> <TableBody> {candidates.map((row) => ( <TableRow key={row.name} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > <TableCell component="th" scope="row"> {row.name} </TableCell> <TableCell align="right">{row.votes}</TableCell> <TableCell align="center"> <Button color="success" variant="contained" onClick={() => { vote(row.name); }} > Vote </Button> {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && <Button color="error" variant="contained" onClick={() => { removeCandidate(row.name); }} > Remove Candidate </Button> } </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> <div className="generalPadding"> <Typography variant="normal" gutterBottom> {winner} is winning the election. </Typography> </div> </div> )} </div> </div> </div> ); } export default App;
要启动基于 React 的 Dapp,可以使用 Yarn CLI:
yarn start
编译和验证后,应用程序将出现在屏幕上,如下所示:
视频期间:
自部署智能合约以来,任何人都可以通过以下 URL 查看完整的历史记录:
https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2
自 2021 年以来,我一直在努力遵循以下使命宣言,我认为它适用于任何技术专业人士:
“将时间集中在提供扩展知识产权价值的特性/功能上。将框架、产品和服务用于其他一切。”
J·维斯特
智能合约提供了允许两方签订协议的能力,其中合约的结果成为交易的固定正式记录。采用智能合约符合我的个人使命,因为底层框架避免在需要此类合约时重新发明轮子。
同时,智能合约设计本身更进一步,从可重用性因素满足了我的使命宣言。在这个例子中,可以使用相同的 HOA 智能合约,尽管在当前选举中有不同的候选人。在这里,我们利用智能合约的力量来避免每次选举时都创建新的智能合约。
当使用 Etherscan 使用 Google 的 ETH 到 USD 转换器查找其中一笔交易的转换价值时,每笔交易的成本为 0.24 (USD) 对应 0.0001348975 ETH。具有讽刺意味的是,这是我小时候从口香糖机中取出一个普通口香糖的成本。
如果您想了解有关智能合约的更多信息, ConsenSys团队提供了出色的资源来帮助您将想法原型化,以查看采用智能合约是否是一个有效的用例。
如果您对本文的源代码感兴趣,可以在以下 URL 找到它:
https://github.com/paul-mcaviney/smart-contract-deep-dive/blob/main/HOABallot.sol
https://gitlab.com/johnjvester/hoa-ballot-contract
https://gitlab.com/johnjvester/hoa-ballot-client
有一个非常棒的一天!