本文将引导你从零开始,构建一个基于以太坊的简易投票DApp。通过智能合约实现投票逻辑,并利用Web3.js与前端页面进行交互,适合以太坊开发初学者入门实践。
环境配置与工具安装
在开始开发前,需要安装以下基础开发环境与工具:
- Node.js 与 npm:用于管理项目依赖和运行JavaScript代码
- Git:版本控制系统,用于代码管理
- Web3.js:以太坊JavaScript API库,实现前端与区块链交互
- solc:Solidity编译器,用于编译智能合约
- TestRPC:本地以太坊测试网络(现称为Ganache CLI)
使用npm全局安装所需工具:
npm install -g web3 solc ganache-cli启动本地测试环境
首先启动本地测试区块链网络:
ganache-cli启动后,系统会自动生成10个测试账户,每个账户预分配100个测试以太币(ETH)。记下提供的HTTP Provider地址(通常为http://localhost:8545),后续将用于连接区块链网络。
编写与编译智能合约
创建投票合约
使用Solidity语言编写投票智能合约,保存为Voting.sol文件:
pragma solidity ^0.4.11;
contract Voting {
mapping (bytes32 => uint8) public votesReceived;
bytes32[] public candidateList;
function Voting(bytes32[] candidateNames) {
candidateList = candidateNames;
}
function totalVotesFor(bytes32 candidate) returns (uint8) {
if (validCandidate(candidate) == false) throw;
return votesReceived[candidate];
}
function voteForCandidate(bytes32 candidate) {
if (validCandidate(candidate) == false) throw;
votesReceived[candidate] += 1;
}
function validCandidate(bytes32 candidate) returns (bool) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}编译智能合约
打开Node.js命令行,编译刚编写的合约:
Web3 = require('web3')
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
solc = require('solc')
fs = require('fs')
code = fs.readFileSync('Voting.sol').toString()
compiledCode = solc.compile(code)部署智能合约到区块链
准备部署信息
获取合约ABI接口定义和字节码:
abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
byteCode = compiledCode.contracts[':Voting'].bytecode部署合约实例
创建合约对象并部署到区块链:
VotingContract = web3.eth.contract(abiDefinition)
deployedContract = VotingContract.new(
['Rama','Nick','Jose'],
{data: byteCode, from: web3.eth.accounts[0], gas: 4700000}
)
contractInstance = VotingContract.at(deployedContract.address)记下返回的合约地址,后续前端交互需要使用。
与智能合约交互
命令行测试
在部署完成后,可通过命令行直接与合约交互:
// 查询候选人得票数
contractInstance.totalVotesFor.call('Rama')
// 为候选人投票
contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
// 再次查询得票数
contractInstance.totalVotesFor.call('Rama').toLocaleString()构建前端界面
创建HTML页面(index.html)提供用户界面:
<!DOCTYPE html>
<html>
<head>
<title>投票DApp</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/web3.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>SSC投票应用</h1>
<table>
<tr><th>候选人</th><th>得票数</th></tr>
<tr><td>Rama</td><td id="candidate-1">0</td></tr>
<tr><td>Nick</td><td id="candidate-2">0</td></tr>
<tr><td>Jose</td><td id="candidate-3">0</td></tr>
</table>
<input type="text" id="candidate" placeholder="输入候选人名字"/>
<button onclick="voteForCandidate()">投票</button>
<script src="index.js"></script>
</body>
</html>创建JavaScript文件(index.js)处理前端逻辑:
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
// 合约ABI接口
abi = JSON.parse('[{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"x","type":"bytes32"}],"name":"bytes32ToString","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"type":"constructor"}]')
VotingContract = web3.eth.contract(abi);
// 使用你的合约地址替换下面的地址
contractInstance = VotingContract.at('0x4131a0f92d36932d3ec3b7a0581546f2e662ad0b');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
function voteForCandidate() {
candidateName = $("#candidate").val();
contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
let div_id = candidates[candidateName];
$("#" + div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
});
}
$(document).ready(function() {
candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
let val = contractInstance.totalVotesFor.call(name).toString()
$("#" + candidates[name]).html(val);
}
});运行与测试DApp
- 确保ganache-cli正在运行
- 在浏览器中打开
index.html文件 - 在输入框中输入候选人姓名(Rama、Nick或Jose)
- 点击"投票"按钮,观察票数变化
- 可通过MetaMask等钱包查看交易状态和余额变化
常见问题
什么是智能合约?
智能合约是运行在区块链上的自执行代码,包含预设规则和协议。一旦部署到区块链,就无法更改,确保了交易的透明性和不可篡改性。
为什么需要本地测试环境?
本地测试环境如Ganache允许开发者在不消耗真实加密货币的情况下测试DApp功能,提供了快速迭代和调试的安全沙盒环境。
Web3.js的主要作用是什么?
Web3.js是以太坊的JavaScript库,提供了与以太坊区块链交互的接口。它允许前端应用读取区块链数据、发送交易并与智能合约进行交互。
如何将DApp部署到主网?
将DApp部署到主网需要以下步骤:1) 使用Truffle或Hardhat等框架编译和优化合约;2) 配置Infura等节点服务;3) 使用MetaMask连接主网;4) 支付Gas费部署合约;5) 更新前端配置指向主网合约地址。
投票DApp有哪些实际应用场景?
投票DApp可用于社区治理、DAO组织决策、民意调查、股东投票等场景,提供透明、可验证且不可篡改的投票机制,确保投票过程的公正性和结果的可信度。
如何改进这个基础投票DApp?
可改进的方向包括:添加投票者身份验证、实施投票权重机制、设置投票时间限制、增加匿名投票功能、优化Gas费用效率以及添加结果可视化图表等高级功能。
通过本教程,你已经掌握了构建基础以太坊DApp的核心流程。继续探索智能合约开发和前端集成,可以创建更复杂且功能丰富的去中心化应用。