以太坊作为全球领先的智能合约平台,为去中心化应用(Dapps)的开发提供了强大的基础设施,本文将通过一个具体的开发实例——构建一个简单的以太坊投票DApp,带大家领略以太坊开发的实际流程,我们将涵盖智能合约编写、编译、部署以及与前端交互的基本步骤。
我们的目标是创建一个去中心化的投票系统,该系统允许以太坊钱包地址对预选的候选人进行投票,并确保每个地址只能投一票,投票结果将实时记录在以太坊区块链上,保证透明和不可篡改性。
在开始之前,请确保你的开发环境已安装以下工具:
安装步骤相对 straightforward,请参考各工具的官方文档进行安装。

智能合约是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 免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如有疑问请发送邮件至:bangqikeconnect@gmail.com