以太坊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 = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"
          candidatesResults.append(candidateTemplate);

          // Render candidate ballot option
          var candidateOption = "<option value='" + id + "' >" + name + "</ option>"
          candidatesSelect.append(candidateOption);
        });
      }
      return electionInstance.voters(App.account);
    }).then(function(hasVoted) {
      // Do not allow a user to vote
      if (hasVoted) {
        $('form').hide();
      }
      loader.hide();
      content.show();
    }).catch(function(error) {
      console.warn(error);
    });
  },

  castVote: function() {
    var candidateId = $('#candidatesSelect').val();
    App.contracts.Election.deployed().then(function(instance) {
      return instance.vote(candidateId, {
        from: App.account
      });
    }).then(function(result) {
      // Wait for votes to update
      $("#content").hide();
      $("#loader").show();
    }).catch(function(err) {
      console.error(err);
    });
  }
};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

二、重新部署智能合约和运行

注意:重启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即可

因为我是重启ganache后重新部署的,所以区块也重新生成,第3个区块包含了新部署合约的地址信息


输入npm run dev 观察页面,重新登录之前注册的metamask账户,在投票时发现confirm交易后会报错:

Error: the tx doesn't have the correct nonce

重新选择metamask的网络,输入http://localhost:7545,再用之前的账户投票,仍然不成功,后来想到可能是因为之前已经投过票了

导入一个新账户,再投票,就能交易成功,且能自动更新,不用手动刷新了。如果页面没反应,请重启chrome浏览器

观察ganache发现生成了一个新区块,其中的发送地址为上述投票的账户地址,接受地址是我们的合约地址。

我在metamask上注册的账户因为以太币为0而不能投票,我用这个新导入的账户给其转入50以太币,转账的交易在ganache中也可以看到,从发送地址到接受账户地址,交易金额为50以太币:

然后用535开头的这个账户进行投票,可以投票成功

投票的交易也在ganache中记录,发送账户为535开头的地址,接受地址为合约地址:

但是投票之后用别的账户登录,再刷新界面时,显示不正常:

看到项目源码下的评论,可以解决这个问题: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();
      });
    });
  }, 

至此,教程结束

猜你喜欢

转载自blog.csdn.net/u011680118/article/details/82453827