以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件

整个教程最终完整代码:https://download.csdn.net/download/u011680118/10649069
一、更新Election.sol智能合约
本教程的最后一步是在投票发生时触发事件,这能帮助我们动态的更新前台界面,更新后的智能合约如下:

pragma solidity ^0.4.2; contract Election { //候选者结构体 struct Candidate { uint id; string name; uint voteCount; } //候选者id到结构体的映射 mapping(uint => Candidate) public candidates; //投票者地址到是否投票的映射 mapping(address => bool) public voters; //总共多少候选者 uint public candidatesCount; //定义投票事件 event votedEvent(uint indexed _candidateId); //构造函数 constructor() public { addCandidate("Candidate 1"); addCandidate("Candidate 2"); } //添加候选者 function addCandidate(string _name) private { candidatesCount ++; candidates[candidatesCount] = Candidate(candidatesCount,_name,0); } //投票函数 function vote(uint _candidateId) public { //要求投票者从没投过票 require(!voters[msg.sender]); //msg.sender是调用这个函数的账户 //要求候选的Id合法 require(_candidateId > 0 && _candidateId <= candidatesCount); //确定投票 voters[msg.sender] = true; //更新候选者票数 candidates[_candidateId].voteCount ++; //触发投票事件,原教程没有emit,我编译会出错 emit votedEvent(_candidateId); } }

更新后的测试文件election.js如下:
var Election = artifacts.require("./Election.sol"); contract("Election", function(accounts) { var electionInstance; //初始化有两个候选者 it("initializes with two candidates", function() { return Election.deployed().then(function(instance) { return instance.candidatesCount(); }).then(function(count) { assert.equal(count, 2); }); }); //候选者初始化信息是否正确 it("it initializes the candidates with the correct values", function() { return Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.candidates(1); }).then(function(candidate) { assert.equal(candidate[0], 1, "contains the correct id"); assert.equal(candidate[1], "Candidate 1", "contains the correct name"); assert.equal(candidate[2], 0, "contains the correct votes count"); return electionInstance.candidates(2); }).then(function(candidate) { assert.equal(candidate[0], 2, "contains the correct id"); assert.equal(candidate[1], "Candidate 2", "contains the correct name"); assert.equal(candidate[2], 0, "contains the correct votes count"); }) }); //测试是否允许投票者进行投票 it("allows a voter to cast a vote", function() { return Election.deployed().then(function(instance) { electionInstance = instance; candidateId = 1; return electionInstance.vote(candidateId, { from: accounts[0] }); }).then(function(receipt) { return electionInstance.voters(accounts[0]); }).then(function(voted) { assert(voted, "the voter was marked as voted"); return electionInstance.candidates(candidateId); }).then(function(candidate) { var voteCount = candidate[2]; assert.equal(voteCount, 1, "increments the candidate's vote count"); }) }); //测试对于非合法候选者进行投票 it("throws an exception for invalid candidates", function() { return Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.vote(99, { from: accounts[1] }) }).then(assert.fail).catch(function(error) { assert(error.message.indexOf('revert') >= 0, "error message must contain revert"); return electionInstance.candidates(1); }).then(function(candidate1) { var voteCount = candidate1[2]; assert.equal(voteCount, 1, "candidate 1 did not receive any votes"); return electionInstance.candidates(2); }).then(function(candidate2) { var voteCount = candidate2[2]; assert.equal(voteCount, 0, "candidate 2 did not receive any votes"); }); }); //测试能否重复投票 it("throws an exception for double voting", function() { return Election.deployed().then(function(instance) { electionInstance = instance; candidateId = 2; electionInstance.vote(candidateId, { from: accounts[1] }); return electionInstance.candidates(candidateId); }).then(function(candidate) { var voteCount = candidate[2]; assert.equal(voteCount, 1, "accepts first vote"); // Try to vote again return electionInstance.vote(candidateId, { from: accounts[1] }); }).then(assert.fail).catch(function(error) { assert(error.message.indexOf('revert') >= 0, "error message must contain revert"); return electionInstance.candidates(1); }).then(function(candidate1) { var voteCount = candidate1[2]; assert.equal(voteCount, 1, "candidate 1 did not receive any votes"); return electionInstance.candidates(2); }).then(function(candidate2) { var voteCount = candidate2[2]; assert.equal(voteCount, 1, "candidate 2 did not receive any votes"); }); }); //测试是否触发投票事件 it("allows a voter to cast a vote", function() { return Election.deployed().then(function(instance) { electionInstance = instance; candidateId = 1; return electionInstance.vote(candidateId, { from: accounts[0] }); }).then(function(receipt) { assert.equal(receipt.logs.length, 1, "an event was triggered"); assert.equal(receipt.logs[0].event, "votedEvent", "the event type is correct"); assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, "the candidate id is correct"); return electionInstance.voters(accounts[0]); }).then(function(voted) { assert(voted, "the voter was marked as voted"); return electionInstance.candidates(candidateId); }).then(function(candidate) { var voteCount = candidate[2]; assert.equal(voteCount, 1, "increments the candidate's vote count"); }) }); });

测试代码监控vote函数返回的交易receipt以保证其有日志信息,这些日志包括了被触发的事件信息,我们检查事件类型是否正确,参数是否正确。
现在开始更新前台代码,以监听投票事件,并且同时更新页面,我们在app.js中添加一个"listenForEvents"函数,如下所示:
listenForEvents: function() { App.contracts.Election.deployed().then(function(instance) { instance.votedEvent({}, { fromBlock: 0, toBlock: 'latest' }).watch(function(error, event) { console.log("event triggered", event) // Reload when a new vote is recorded App.render(); }); }); }

这个函数通过调用"votedEvent"函数以监听投票事件,然后传入一些参数数据保证监听区块链上的所有事件,然后监听的时候打印事件信息, 发生投票事件后进行页面更新,从loading界面到正常界面,同时显示得票数,并隐藏投票功能。对于初始化函数进行更新,如下:
initContract: function() { $.getJSON("Election.json", function(election) { // Instantiate a new truffle contract from the artifact App.contracts.Election = TruffleContract(election); // Connect provider to interact with contract App.contracts.Election.setProvider(App.web3Provider); App.listenForEvents(); return App.render(); }); }

完整的app.js代码如下:
App = { web3Provider: null, contracts: {}, account: '0x0', hasVoted: false,init: function() { return App.initWeb3(); },initWeb3: function() { // TODO: refactor conditional if (typeof web3 !== 'undefined') { // If a web3 instance is already provided by Meta Mask. App.web3Provider = web3.currentProvider; web3 = new Web3(web3.currentProvider); } else { // Specify default instance if no web3 instance provided App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545'); web3 = new Web3(App.web3Provider); } return App.initContract(); },initContract: function() { $.getJSON("Election.json", function(election) { // Instantiate a new truffle contract from the artifact App.contracts.Election = TruffleContract(election); // Connect provider to interact with contract App.contracts.Election.setProvider(App.web3Provider); App.listenForEvents(); return App.render(); }); },// Listen for events emitted from the contract listenForEvents: function() { App.contracts.Election.deployed().then(function(instance) { // Restart Chrome if you are unable to receive this event // This is a known issue with Metamask // https://github.com/MetaMask/metamask-extension/issues/2393 instance.votedEvent({}, { fromBlock: 0, toBlock: 'latest' }).watch(function(error, event) { console.log("event triggered", event) // Reload when a new vote is recorded App.render(); }); }); },render: function() { var electionInstance; var loader = $("#loader"); var content = $("#content"); loader.show(); content.hide(); // Load account data web3.eth.getCoinbase(function(err, account) { if (err === null) { App.account = account; $("#accountAddress").html("Your Account: " + account); } }); // Load contract data App.contracts.Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.candidatesCount(); }).then(function(candidatesCount) { var candidatesResults = $("#candidatesResults"); candidatesResults.empty(); var candidatesSelect = $('#candidatesSelect'); candidatesSelect.empty(); for (var i = 1; i <= candidatesCount; i++) { electionInstance.candidates(i).then(function(candidate) { var id = candidate[0]; var name = candidate[1]; var voteCount = candidate[2]; // Render candidate Result var candidateTemplate = "" + id + "" + name + "" + voteCount + "" candidatesResults.append(candidateTemplate); // Render candidate ballot option var candidateOption = "

二、重新部署智能合约和运行
注意:重启ganache,之前部署的智能合约都会丢失,需要重新部署,会从头生成区块
输入truffle migrate重新部署时报错:Error: Attempting to run transaction which calls a contract function, but recipient address 0x3e90490273dd38550d590194877d2162a8d0932f is not a contract address
解决办法,就是将程序根目录(election目录)下的build文件夹删除,然后重新truffle migrate即可
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

【以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件】因为我是重启ganache后重新部署的,所以区块也重新生成,第3个区块包含了新部署合约的地址信息
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

输入npm run dev 观察页面,重新登录之前注册的metamask账户,在投票时发现confirm交易后会报错:
Error: the tx doesn't have the correct nonce
重新选择metamask的网络,输入http://localhost:7545,再用之前的账户投票,仍然不成功,后来想到可能是因为之前已经投过票了
导入一个新账户,再投票,就能交易成功,且能自动更新,不用手动刷新了。如果页面没反应,请重启chrome浏览器

以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

观察ganache发现生成了一个新区块,其中的发送地址为上述投票的账户地址,接受地址是我们的合约地址。
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

我在metamask上注册的账户因为以太币为0而不能投票,我用这个新导入的账户给其转入50以太币,转账的交易在ganache中也可以看到,从发送地址到接受账户地址,交易金额为50以太币:
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

然后用535开头的这个账户进行投票,可以投票成功
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

投票的交易也在ganache中记录,发送账户为535开头的地址,接受地址为合约地址:
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片


但是投票之后用别的账户登录,再刷新界面时,显示不正常:
以太坊dApp开发教程(如何一步步构造一个全栈式去中心化应用)(五)监听事件
文章图片

看到项目源码下的评论,可以解决这个问题:https://github.com/dappuniversity/election/issues/2
把app.js中的监听函数改一下,从最新的区块监听,如果从第一个区块开始监听,会有旧的事件,再更新页面就会出现重复
listenForEvents: function() { App.contracts.Election.deployed().then(function(instance) { // Restart Chrome if you are unable to receive this event // This is a known issue with Metamask // https://github.com/MetaMask/metamask-extension/issues/2393 instance.votedEvent({}, { fromBlock: 'latest',//原来是0,会导致候选者列表重复出现,改成latest就正常了 toBlock: 'latest' }).watch(function(error, event) { console.log("event triggered", event) // Reload when a new vote is recorded App.render(); }); }); },

重新部署合约,对于之前用过的账户,进行投票时会报错,显示transaction failed:the tx doesn't have the correct nonce之类的信息,解决方法是打开metamask的账户,点击设置,然后reset账户,清空之前的transaction信息,再投票即可。

解决方法来源:https://consensys.zendesk.com/hc/en-us/articles/360004177531-Resetting-an-Account-New-UI-
至此,教程结束

    推荐阅读