基于node、ganache搭建一个以太坊投票Dapp
前言
这段时间在remix中学习solidity语法,虽然remix很强大,但是编写好的合约代码很容易丢失,虽然remix可以使用本地文件系统及github中的远程仓库,但还是想尝试一下在本地开发智能合约及网页与智能合约交互的过程
话不多说,本文主要介绍一个基于以太坊的智能合约投票dapp的一个完整流程
工具及技术框架
-
智能合约编写工具:Remix,个人喜欢用remix写智能合约,写好智能合约直接部署测试,把写好的合约代码再放到拷贝出来放到项目中进行编译
-
前端页面编写工具:vscode
-
环境:node:v14.17.0、npm:6.14.13
-
依赖包:
"ganache-cli": "^6.12.2", "solc": "^0.4.22","web3": "^1.3.6"
关于几个依赖包的简要介绍:
Ganache CLI
Ganache CLI是以太坊开发工具Truffle套件的一部分,是以太坊开发私有区块链的Ganache命令行版本,用于测试和开发的快速以太坊RPC客户端,用于测试和开发的快速以太坊RPC客户端。ganache-cli是用Javascript编写的,并通过npm作为Node包进行分发。安装之前首先要确保安装了Node.js(> = v6.11.5)。
旨在快速搭建一个小demo,就没有用geth客户端去搭建一个本地私有链,直接用Ganache建一个干净纯粹的模拟节点进行测试,跟在Remix上用JavaScript VM测试合约是一个原理
solc
solidity编写的以太坊智能合约可通过命令行编译工具solc来进行编译,成为以太坊虚拟机中的代码。solc编译后最终部署到链上形成我们所见到的各种智能合约。
web3
web3.js是一个库集合,你可以使用HTTP或IPC连接本地或远程以太它节点进行交互。 web3的JavaScript库能够与以太坊区块链交互。 它可以检索用户帐户,发送交易,与智能合约交互等。
以上的三个依赖包具体的信息就不详细介绍
工程结构
首先使用node init
初始化一个node项目,这里取名字为simple_voting_dapp
项目根目录安装依赖:npm i ganache-cli@6.12.2 solc@0.4.22 web3@1.3.6
安装完成后package.json应该是如下信息
{
"name": "simple_voting_dapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"ganache-cli": "^6.12.2",
"solc": "^0.4.22",
"web3": "^1.3.6"
}
}
接着创建如下目录结构及代码
投票合约
本例子中的投票合约是一个非常简单的合约,代码如下:
pragma solidity ^0.4.22;
contract Voting {
bytes32[] public canditateList;
mapping (bytes32 => uint8) public votesReceived;
constructor() public {
canditateList = getBytes32ArrayForInput();
}
function validateCandidate(bytes32 candiateName) internal view returns(bool){
for(uint8 i = 0;i<canditateList.length; i++){
if(candiateName == canditateList[i])
return true;
}
return false;
}
function vote(bytes32 candiateListName) public payable returns(bytes32){
require(validateCandidate(candiateListName));
votesReceived[candiateListName] +=1;
}
function totalVotesFor(bytes32 candiateName) public view returns(uint8){
require(validateCandidate(candiateName));
return votesReceived[candiateName];
}
function getBytes32ArrayForInput() pure public returns (bytes32[3] b32Arr) {
b32Arr = [bytes32("Candidate"), bytes32("Alice"), bytes32("Cary")];
}
}
变量说明:
canditateList:投票的对象
votesReceived:投票的对象—>获得的总票数。一个映射
构造函数:
调用getBytes32ArrayForInput函数初始化三个投票对象至canditateList中
函数
validateCandidate:一个internal、view的函数,返回目标是否在投票对象列表中
vote:给制定投票对象进行投票
totalVotesFor:查看指定对象的获得票数
编译脚本compile.js
const fs = require('fs-extra');
const path = require('path');
const solc = require('solc');
// cleanup 清除之前已编译的文件
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir);
fs.ensureDirSync(compiledDir);
// compile 开始编译合约
const contractPath = path.resolve(__dirname, '../../contract',
'voting.sol');
const contractSource = fs.readFileSync(contractPath, 'utf8');
const result = solc.compile(contractSource, 1);
// check errors 检查错误
if (Array.isArray(result.errors) && result.errors.length) {
throw new Error(result.errors[0]);
}
// save to disk 保存到指定目录compiled目录中
Object.keys(result.contracts).forEach( name => {
const contractName = name.replace(/^:/, '');
const filePath = path.resolve(__dirname, '../compiled',
`${contractName}.json`);
fs.outputJsonSync(filePath, result.contracts[name]);
console.log(`save compiled contract ${contractName} to
${filePath}`);
});
部署脚本deploy.js
const path = require('path');
const Web3 = require('web3');
// 连接到ganache启动的节点
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
// 1. 拿到 abi 和 bytecode
const contractPath = path.resolve(__dirname, '../compiled/Voting.json');
const { interface, bytecode } = require(contractPath);
(async () => {
// 2. 获取钱包里面的账户
const accounts = await web3.eth.getAccounts();
console.log('部署合约的账户:', accounts[0]);
// 3. 创建合约实例并且部署
console.time('合约部署耗时');
var result = await new
web3.eth.Contract(JSON.parse(interface))
.deploy({data: bytecode})
.send({
from: accounts[0],
gas: 1500000,
gasPrice: '30000000000000'
})
.then(function(newContractInstance){
console.log('合约部署成功: ' + newContractInstance.options.address) // instance with the new contract address
});
console.timeEnd('合约部署耗时');
})();
部署合约
1.手动启动ganache程序,运行一个节点,保持终端不关闭
.\node_modules\.bin\ganache-cli
2.编译voting.sol投票合约
node .\contract_workflow\scripts\compile.js
contract_workflow目录下面会出现Voting.json文件,后面index.js会用到该文件中的内容
3.部署合约
node .\contract_workflow\scripts\deploy.js
会出现以下信息
部署合约的账户: 0x8C6ba0616f05909eCb334C1a3707C38a8d7bDd0F
合约部署成功: 0xaCCBC818BC9224A6da2902eeF82c3d14afC82aB5
合约部署耗时: 345.211ms
网页交互代码index.html
<!DOCTYPE html>
<html>
<head>
<title>Voting DApp</title>
<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
</head>
<body class="container">
<h1>A Simple Voting Application</h1>
<div class="table-responsive"></div>
<table class="table table-bordered">
<thead>
<tr>
<th>Candidate</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Candidate</td>
<td id="candidate-1"></td>
</tr>
<tr>
<td>Alice</td>
<td id="candidate-2"></td>
</tr>
<tr>
<td>Cary</td>
<td id="candidate-3"></td>
</tr>
</tbody>
</table>
</div>
<input type="text" id="candidate" />
<a href="#" onclick="vote()" class="btn btn-primary">Vote</a>
</body>
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js">
</script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js">
</script>
<script src="./index.js"></script>
</html>
网页交互index.js
要注意contractAddr
和abi
两个变量的值,
contractAddr是在部署合约时获取的
abi是从Voting.json中的interface获取的,打开Voting.json即可看到interface对象内容
// const path = require('path');
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
// const contractPath = path.resolve(__dirname, '../contract_workflow/compiled/Voting.json');
// 部署的只能合约地址
contractAddr = "0xaCCBC818BC9224A6da2902eeF82c3d14afC82aB5";
// abi内容,从Voting.json中拷贝interface对象的内容,由于内容过程,这里就省略了
abi = ""
contractInstance = new web3.eth.Contract(JSON.parse(abi),contractAddr);
candidates = { "Candidate": "candidate-1", "Alice": "candidate-2", "Cary": "candidate-3" };
let accounts;
web3.eth.getAccounts().then(result => accounts = result);
function vote() {
let candidateName = $("#candidate").val();
try {
contractInstance.methods.vote(stringtoHex(candidateName)).send({from: accounts[0] })
.on('transactionHash', function(hash){
console.log("hash:" + hash)
contractInstance.methods.totalVotesFor(stringtoHex(candidateName)).call().then(result => $("#" + candidates[candidateName]).html(result));
})
.on('confirmation', function(confirmationNumber, receipt){
// console.log(confirmationNumber,receipt)
})
.on('receipt', function(receipt){
// receipt example
// console.log("receipt" +receipt);
})
.on('error', console.error);
} catch (err) {
console.log(err)
}
}
// 需转成16进制的才能给合约中byte32的数据传参,且前缀要加上0x
var stringtoHex = function (str) {
var val = "";
for (var i = 0; i < str.length; i++) {
if (val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return "0x"+val
}
$(document).ready(function () {
candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
contractInstance.methods.totalVotesFor(stringtoHex(name)).call().then(result => $("#" + candidates[name]).html(result));
}
});
在本地打开src/index.html,出现以下界面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOhRyuLm-1625021855675)(/img/voting2.png)]
在输入框中输入对应投票对象的名字,点击Vote
按钮即可为指定对象增加票数
至此,一个完整的demo就完成了!!
小结
本项目是一个比较基础的区块链demo,但是还有很多要优化的地方
- 基于ganache启动的节点在关闭后,需要重新部署合约
- 部署合约编译部署分为了两步,很繁琐
- 部署合约后要手动修改index.js中合约地址和abi
所以自动化的流程还是比较繁琐的
但是通过这个小demo,可以学到的东西还是很多的,比如如何用node通过ganache启动一个本地私有节点、如何编译合约、部署合约、调用合约
后期会介绍一个更自动化、更简便的框架Truffle去实现以上步骤,通过Truffle框架把以上很多编译,部署的脚本都帮我们完成了,所以学了这个demo后,学习truffle会更加得心应手
感谢
感谢尚硅谷的区块链教程:https://www.bilibili.com/video/BV1NJ411D7rf