从零开始,构建一个简单的以太坊投票DApp开发实例
以太坊作为全球领先的智能合约平台,为去中心化应用(DApps)的开发提供了强大的基础设施,本文将通过一个具体的开发实例——构建一个简单的以太坊投票DApp,带大家领略以太坊开发的实际流程,我们将涵盖智能合约编写、编译、部署以及与前端交互的基本步骤。
项目概述:投票DApp
我们的目标是创建一个去中心化的投票系统,该系统允许以太坊钱包地址对预选的候选人进行投票,并确保每个地址只能投一票,投票结果将实时记录在以太坊区块链上,保证透明和不可篡改性。
开发环境准备
在开始之前,请确保你的开发环境已安装以下工具:
- Node.js 和 npm: 用于运行JavaScript环境和包管理。
- Truffle Suite: 最流行的以太坊开发框架,包含智能合约编译、测试和部署工具。
- Ganache: 一条本地的以太坊区块链,方便快速开发和测试,它会为你提供10个测试账户,每个账户都有100个ETH。
- MetaMask: 浏览器插件钱包,用于与以太坊网络交互,以及在DApp中进行签名和交易。
- 代码编辑器: 如 VS Code。
安装步骤相对 straightforward,请参考各工具的官方文档进行安装。
智能合约开发 (Solidity)
智能合约是DApp的核心逻辑所在,我们使用Solidity语言编写。
-
创建Truffle项目:
mkdir ethereum-voting-dapp cd ethereum-voting-dapp truffle init
-
编写合约代码: 在
contracts目录下创建一个新的文件Voting.sol。// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract Voting { // 定义候选人结构体 struct Candidate { uint id; string name; uint voteCount; } // 存储候选人信息的映射 mapping(uint => Candidate) public candidates; // 存储投票者是否已投票的映射 mapping(address => bool) public voters; // 候选人数量 uint public candidatesCount; // 事件:当候选人得票时触发 event VotedEvent(uint indexed candidateId, address voter, uint voteCount); // 构造函数,初始化候选人 constructor(string[] memory candidateNames) { candidatesCount = 0; for (uint i = 0; i < candidateNames.length; i++) { candidates[candidatesCount] = Candidate(candidatesCount, candidateNames[i], 0); candidatesCount++; } } // 投票函数 function vote(uint _candidateId) public { // 确保投票者尚未投票 require(!voters[msg.sender], "You have already voted."); // 确保候选人ID有效 require(_candidateId < candidatesCount, "Invalid candidate ID."); // 标记投票者已投票 voters[msg.sender] = true; // 增加候选人得票数 candidates[_candidateId].voteCount++; // 触发事件 emit VotedEvent(_candidateId, msg.sender, candidates[_candidateId].voteCount); } // 获取候选人信息 function getCandidate(uint _candidateId) public view returns (uint id, string memory name, uint voteCount) { Candidate storage candidate = candidates[_candidateId]; return (candidate.id, candidate.name, candidate.voteCount); } }合约解析:
Candidate结构体存储候选人的ID、姓名和得票数。candidates映射根据ID存储候选人信息。voters映射记录每个地址是否已投票,防止重复投票。constructor在合约部署时初始化候选人列表。vote(uint _candidateId)是核心投票函数,包含权限检查和状态修改。getCandidate用于查询特定候选人的信息。
编译与测试智能合约
-
编译合约: 在项目根目录下运行:
truffle compile
成功后,
build/contracts目录下会生成相应的JSON文件,这是合约的ABI(应用二进制接口)和字节码。 -
编写测试用例 (可选但推荐): 在
test目录下创建voting.test.js(例如使用JavaScript测试框架)。const Voting = artifacts.require("Voting"); contract("Voting", accounts => { it("should initialize with the correct candidate names", async () => { const votingInstance = await Voting.deployed(); const candidateCount = await votingInstance.candidatesCount(); assert.equal(candidateCount.toNumber(), 2, "There should be 2 candidates"); const candidate0 = await votingInstance.getCandidate(0); assert.equal(candidate0[1], "Candidate 1", "First candidate name is incorrect"); const candidate1 = await votingInstance.getCandidate(1); assert.equal(candidate1[1], "Candidate 2", "Second candidate name is incorrect"); }); it("should allow a voter to cast a vote", async () => { const votingInstance = await Voting.deployed(); const voter = accounts[0]; // 初始票数 const candidate0InitialVotes = (await votingInstance.getCandidate(0))[2]; await votingInstance.vote(0, { from: voter }); // 投票后票数 const candidate0NewVotes = (await votingInstance.getCandidate(0))[2]; assert.equal(candidate0NewVotes.toNumber(), candidate0InitialVotes.toNumber() + 1, "Candidate vote count did not increase"); // 检查投票者状态 const hasVoted = await votingInstance.voters(voter); assert.equal(hasVoted, true, "Voter status is not set to true"); }); it("should not allow a voter to vote more than once", async () => { const votingInstance = await Voting.deployed(); const voter = accounts[1]; // 第一次投票 await votingInstance.vote(1, { from: voter }); // 尝试第二次投票,应该失败 try { await votingInstance.vote(0, { from: voter }); assert.fail("Expected revert but did not revert"); } catch (error) { assert.include(error.message, "You have already voted.", "Expected revert message not found"); } }); }); -
运行测试:
truffle test
部署智能合约
-
配置网络: 在
truffle-config.js(或truffle.js) 中,配置本地网络 (Ganache)。module.exports = { networks: { development: { host: "127.0.0.1", // Localhost (default: none) port: 7545, // Standard Ethereum port (default: none) network_id: "*", // Any network (default: none) }, }, compilers: { solc: { version: "0.8.9", // Fetch exact version from solc-bin (default: truffle's version) }, }, }; -
编写部署脚本: 在
migrations目录下创建一个新的迁移文件,2_deploy_contracts.js。const Voting = artifacts.require("Voting"); module.exports = function (deployer) { // 部署合约时传入候选人名单 deployer.deploy(Voting, ["Candidate 1", "Candidate 2"]); }; -
部署到本地网络: 确保 Ganache 已运行,并点击 "QUICKSTART" 按钮,然后运行:
truffle migrate --network development
成功部署后,控制台会显示合约的地址。
开发前端界面
前端与智能合约交互,我们使用HTML、CSS和JavaScript,并借助 web3.js
ethers.js 库,这里我们以 ethers.js 为例。
-
安装ethers.js:
npm install ethers
-
创建HTML文件: 在
client目录下 (可以手动创建,或使用truffle create react等命令生成React项目,这里简化) 创建index.html。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ethereum Voting DApp</title> <style> body { font-family