从零开始,以太坊开发实例—构建一个简单的投票DApp
时间:
2026-02-25 11:09 阅读数:
1人阅读
以太坊,作为全球领先的智能合约平台,不仅加密货币的基石,更催生了去中心化应用(DApps)的蓬勃发展,对于许多开发者而言,踏入以太坊开发的世界,最有效的方式莫过于通过一个具体的实例来学习,本文将以一个简单但功能完整的“投票DApp”为例,带你走过以太坊开发的核心流程,包括环境搭建、智能合约编写、测试、部署以及与前端交互的初步概念。
开发环境准备:工欲善其事,必先利其器
在开始编码之前,我们需要准备以下开发环境:
- Node.js 和 npm:JavaScript 运行时环境和包管理器,建议从官网下载并安装 LTS 版本。
- Truffle Suite:一套强大的以太坊开发框架,包含智能合约编译、测试、部署等功能。
npm install -g truffle
- Ganache:一个个人区块链,用于本地开发和测试,它会为你提供一个模拟的以太坊网络,并分配 10 个测试账户,每个账户都有 100 个模拟 ETH。
- 可以从 Ganache 官网下载桌面版,或通过
npm install -g ganache命令安装命令行版本。
- 可以从 Ganache 官网下载桌面版,或通过
- MetaMask:浏览器钱包插件,用于与以太坊网络交互(包括本地测试网络和主网),从 Chrome 等浏览器的应用商店安装。
- 代码编辑器:推荐使用 Visual Studio Code,并安装 Solidity 插件,以获得更好的代码提示和高亮。
创建项目结构
- 创建一个新的项目文件夹,
voting-dapp。mkdir voting-dapp cd voting-dapp
- 使用 Truffle 初始化项目:
truffle init
这会创建以下标准目录结构:
contracts/code>:存放 Solidity 智能合约文件。
migrations/:存放部署脚本文件。test/:存放测试文件。truffle-config.js:Truffle 配置文件。
编写智能合约:投票DApp的核心
我们的投票DApp将允许创建投票,并让参与者对特定选项进行投票。
- 在
contracts目录下创建一个新的 Solidity 文件,命名为Voting.sol。 - 编写合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
// 定义投票选项的结构体
struct Option {
string name;
uint voteCount;
}
// 定义提案的结构体
struct Proposal {
string description;
mapping(address => bool) hasVoted; // 记录用户是否已投票
Option[] options; // 投票选项数组
}
// 存储所有提案的映射,key为提案ID,value为提案
mapping(uint => Proposal) public proposals;
uint public proposalCount;
// 提案创建者
address public owner;
// 构造函数,设置合约所有者
constructor() {
owner = msg.sender;
}
// 创建新提案
function createProposal(string memory _description, string[] memory _optionNames) public {
require(msg.sender == owner, "Only owner can create proposals");
proposalCount++;
Proposal storage proposal = proposals[proposalCount];
proposal.description = _description;
for (uint i = 0; i < _optionNames.length; i++) {
proposal.options.push(Option({
name: _optionNames[i],
voteCount: 0
}));
}
}
// 对提案的某个选项进行投票
function vote(uint _proposalId, uint _optionIndex) public {
Proposal storage proposal = proposals[_proposalId];
require(!proposal.hasVoted[msg.sender], "You have already voted for this proposal");
require(_proposalId > 0 && _proposalId <= proposalCount, "Proposal does not exist");
require(_optionIndex < proposal.options.length, "Option does not exist");
proposal.hasVoted[msg.sender] = true;
proposal.options[_optionIndex].voteCount++;
}
// 获取提案的选项数量
function getProposalOptionsCount(uint _proposalId) public view returns (uint) {
return proposals[_proposalId].options.length;
}
// 获取提案的某个选项信息
function getOption(uint _proposalId, uint _optionIndex) public view returns (string memory, uint) {
return (proposals[_proposalId].options[_optionIndex].name, proposals[_proposalId].options[_optionIndex].voteCount);
}
}
合约解析:
Option结构体:存储投票选项名称和票数。Proposal结构体:存储提案描述、已投票用户记录和选项数组。proposals:映射,用于存储所有提案。proposalCount:提案总数,也用作新提案的ID。owner:提案创建者地址,只有合约所有者可以创建提案。createProposal:创建新提案,传入提案描述和选项名称数组。vote:对指定提案的指定选项进行投票,并确保每个地址只能投一次票。getProposalOptionsCount和getOption:视图函数,用于获取提案的选项信息和投票结果。
编写迁移(部署)脚本
为了让 Truffle 知道如何部署我们的合约,我们需要在 migrations 目录下创建一个新的迁移脚本。
- 在
migrations目录下创建文件2_deploy_contracts.js(数字前缀表示部署顺序)。 - 编写脚本:
const Voting = artifacts.require("Voting");
module.exports = function (deployer) {
// 部署 Voting 合约
// 可以在这里传入初始参数,如果构造函数有的话
// deployer.deploy(Voting, "Initial Proposal Description", ["Option1", "Option2"]);
// 但我们的合约构造函数没有参数,所以直接部署
deployer.deploy(Voting);
};
编写测试
Truffle 使用 Mocha 和 Chai 进行测试,在 test 目录下创建 voting.test.js 文件:
const Voting = artifacts.require("Voting");
contract("Voting", (accounts) => {
let votingInstance;
const owner = accounts[0];
const voter1 = accounts[1];
const voter2 = accounts[2];
beforeEach(async () => {
votingInstance = await Voting.new();
});
it("should have an owner", async () => {
const contractOwner = await votingInstance.owner();
assert.equal(contractOwner, owner, "The owner is not correct");
});
it("should allow the owner to create a proposal", async () => {
const description = "What should we have for lunch?";
const optionNames = ["Pizza", "Sushi", "Burger"];
await votingInstance.createProposal(description, optionNames, { from: owner });
const proposalCount = await votingInstance.proposalCount();
assert.equal(proposalCount.toNumber(), 1, "Proposal count should be 1");
const retrievedDescription = await votingInstance.proposals(1).description;
assert.equal(retrievedDescription, description, "Proposal description is incorrect");
const optionsCount = await votingInstance.getProposalOptionsCount(1);
assert.equal(optionsCount.toNumber(), 3, "Options count is incorrect");
});
it("should allow a voter to vote", async () => {
await votingInstance.createProposal("Favorite color?", ["Red", "Blue", "Green"], { from: owner });
await votingInstance.vote(1, 0, { from: voter1 }); // 投给第一个选项 "Red"
const redVotes = await votingInstance.getOption(1, 0);
assert.equal(redVotes[1].toNumber(), 1, "Red vote count should be 1");
const hasVoted = await votingInstance.proposals(1).hasVoted(voter1);
assert.equal(hasVoted, true, "Voter should have voted");
});
it("should not allow a voter to vote twice", async () => {
await votingInstance.createProposal("Favorite color?", ["Red", "Blue", "Green"], { from: owner });
await votingInstance.vote(1, 0, { from: voter1 });
try {
await votingInstance.vote(1, 1, { from: voter1 });
assert.fail("Expected revert but none was received");
} catch (error) {
assert.include(error.message, "You have already voted for this proposal");
}
});
});
运行测试: 在项目根目录下运行:
truffle test
Truffle 会自动启动本地测试网络(如果未启动),运行测试并输出结果。