区块链技术以其去中心化、透明不可篡改和智能合约自动执行等特性,正在深刻改变众多行业,理解区块链技术的最佳途径之一便是通过实际的编程实例,本文将以一个简单的“去中心化投票应用”(Decentralized Voting App)为例,带你走过从环境搭建、智能合约编写、前端交互到部署测试的全过程,为你揭开区块链应用开发的神秘面纱。
开发环境准备
在开始编写代码之前,我们需要搭建好基本的开发环境:
- Node.js 和 npm:JavaScript 运行时环境和包管理器,从 Node.js 官网 下载并安装 LTS 版本。
- Truffle Suite:最受欢迎的以太坊开发框架,包含智能合约编译、测试、部署等工具,通过 npm 安装:
npm install -g truffle
- Ganache:一款个人区块链,用于本地快速部署和测试以太坊网络,它会提供一系列默认的测试账户和私钥,从 Ganache 官网 下载桌面版或命令行版。
- MetaMask:浏览器钱包插件,用于与区块链交互(如发送交易、连接 DApp),从 MetaMask 官网 安装浏览器插件。
- 代码编辑器:如 VS Code,并安装 Solidity 插件。
项目初始化与智能合约编写
创建项目目录并初始化
mkdir decentralized-voting-app cd decentralized-voting-app truffle init
这会创建一个标准的 Truffle 项目结构,包括 contracts/(智能合约)、migrations/(部署脚本)、test/(测试文件)等目录。
编写智能合约
在 contracts/ 目录下创建一个新的 Solidity 文件 Voting.sol,智能合约是区块链应用的核心,定义了业务逻辑和数据结构。
// contracts/Voting.sol
pragma solidity ^0.8.0;
contract Voting {
// 候选人结构体
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 主席官地址,用于添加候选人
address public chairperson;
// 候选人列表,键为候选人ID
mapping(uint => Candidate) public candidates;
// 投票人地址到是否已投票的映射
mapping(address => bool) public voters;
// 候选人ID计数器
uint public candidatesCount;
// 事件,用于前端监听投票事件
event VotedEvent(uint indexed candidateId, address indexed voter);
// 构造函数,部署时设置主席官
constructor() {
chairperson = msg.sender;
}
// 主席官添加候选人
function addCandidate(string memory _name) public {
require(msg.sender == chairperson, "Only chairperson can add candidates");
candidatesCount++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
// 投票函数
function vote(uint _candidateId) public {
// 确保投票人尚未投票
require(!voters[msg.sender], "You have already voted");
// 确保候选人ID有效
require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID");
voters[msg.sender] = true;
candidates[_candidateId].voteCount++;
// 触发投票事件
emit VotedEvent(_candidateId, msg.sender);
}
// 获取候选人信息
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、姓名和得票数。chairperson是添加候选人的唯一地址,在构造函数中设置为合约部署者。candidates是一个映射,存储所有候选人信息。voters记录每个地址是否已投票,防止重复投票。addCandidate:仅主席官可调用,用于添加新候选人。vote:用户调用,为指定候选人投票,并更新投票状态和候选人得票数。VotedEvent:事件,方便前端监听投票行为。
编写迁移(部署)脚本
在 migrations/ 目录下创建一个新的迁移脚本,2_deploy_voting.js,用于部署我们的 Voting 合约。
// migrations/2_deploy_voting.js
const Voting = artifacts.require("Voting");
module.exports = function (deployer) {
// 部署 Voting 合约
deployer.deploy(Voting);
};
编写测试用例
在 test/ 目录下创建 voting.test.js,使用 Mocha 和 Chai 编写测试用例,确保合约逻辑正确。
// test/voting.test.js
const Voting = artifacts.require("Voting");
contract("Voting", (accounts) => {
let votingInstance;
const chairperson = accounts[0];
const voter1 = accounts[1];
const voter2 = accounts[2];
const candidateName = "Alice";
beforeEach(async () => {
votingInstance = await Voting.new();
await votingInstance.addCandidate(candidateName, { from: chairperson });
});
it("should initialize with correct chairperson", async () => {
const chair = await votingInstance.chairperson();
assert.equal(chair, chairperson, "Chairperson is not correct");
});
it("should allow chairperson to add a candidate", async () => {
const candidateCount = await votingInstance.candidatesCount();
assert.equal(candidateCount, 1, "Candidate count should be 1 after adding one");
const candidate = await votingInstance.getCandidate(1);
assert.equal(candidate[1], candidateName, "Candidate name is not correct");
});
it("should allow a voter to vote", async () => {
await votingInstance.vote(1, { from: voter1 });
const voter = await votingInstance.voters(voter1);
assert.equal(voter, true, "Voter should be marked as voted");
const candidate = await votingInstance.getCandidate(1);
assert.equal(candidate[2], 1, "Candidate vote count should be 1");
});
it("should not allow a voter to vote twice", async () => {
await votingInstance.vote(1, { from: voter1 });
try {
await votingInstance.vote(1, { from: voter1 });
assert.fail("Expected revert when voting twice");
} catch (error) {
assert.include(error.message, "You have already voted");
}
});
});
