《我学区块链》—— 八、用区块链投票

八、用区块链投票

       在该系列文章的最开始部分我们说过,区块链是一种去中心化的数据库。数据库大家都理解,就是用来存数据,而基本上不需要关注存的是什么。即然如此,它的作用就不仅仅是发行代币这一种用法。这次,我们就来做一个用区块链投票的应用。我们会编写一个智能合约,其中包含一个 mapping 对象,其 key 是候选人代码,value 是我们自己定义的候选人对象,内部会包含候选人姓名、所得票数等。同时要记录已经投过票的人,保证其不能重复投票。最后,该应用可以列表展示每位候选人获得的票数,接下来让我们来看一下这个 DAPP。

       注:该案例来源于幕课网的谦益,因为在原文章中有些内容做了一些省略,所以此处除了做重现,也将这部分内容一并补全。

       先放一张图片

用区块链投票

1、开发中要用到的工具

  • node             # Truffle 及 Ganache-cli 将运行在 node 之上
  • npm              # 开发中的项目构建,包管理及临时服务器等需要 npm 工具
  • web3.js         # 可以通过 JavaScript 调用智能合约的工具
  • Truffle           # 智能合约开发框架以及构建工具。提供了合约抽象接口,通过 web3.js 可在 JavaScript 中直接操作合约函数。其还对客户端做了深度集成,开发,测试,部署一行命令都可以搞定。
  • Solidity          # 智能合约语言
  • Ganache-cli  # 私链本地仿真工具,比 Geth 挖矿要快,是 etherumjs-testrpc 的官方替代版

       其中 node 及 npm 在前面的章节中已经安装,此处不再重复。而 solidity 及 web3js 会在 truffle 中包含,无需单独安装,所以此处只需进一步全局安装 ganache-cil 和 truffle。

sudo npm install -g ganache-cli truffle # 安装
ganache-cli     # 启动 ganache-cli

2、初始化工程

       这里偷个懒,在官方的 pet-shop 项目上进行修改。新建一个文件夹,并在该文件夹内执行下面的命令

mkdir pet-shop & cd pet-shop
truffle unbox pet-shop

       这样就通过 truffle 的 unbox 工具初始化了一个官方的宠物商店 Dapp,接下来将在其上开发区块链投票相关的合约及项目展示层。

3、编写合约

       在包 contracts 下面新建一个 Election.sol 的合约文件,并在其中写入如下代码:

pragma solidity ^0.4.23;
contract election {
    // 结构体
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }
    // 事件
    event votedEvent(uint indexed_candidateId);
    // 存储结构体
    mapping(uint => Candidate) public candidates;
    // 是否已经投票了
    mapping(address => bool) public voters;
    // 总数量
    uint public candidateCount;
    // 构造函数
    constructor() public {
        addCandidate("张三");
        addCandidate("李四");
    }
    // 添加候选人
    function addCandidate(string _name) private {
        candidateCount++;
        candidates[candidateCount] = Candidate(candidateCount, _name, 0);
    }
    // 投票
    function vote(uint _candidateId) public {
        // 过滤
        require(!voters[msg.sender]);
        require(_candidateId > 0 && _candidateId <= candidateCount);
        // 记录用户已经投票了
        voters[msg.sender] = true;
        candidates[_candidateId].voteCount++;
        emit votedEvent(_candidateId);
    }
}

       这里对以上代码做一些解释:

       struct:是 solidity 的关键字,用于声明结构体,这里相当于定义了一个内部类,封装出我们需要的 候选人对象;

       event:用于在 solidity 智能合约中定义事件,其可以帮助反过来调用 JavaScript 中监听了该事件的回调。

       mapping:一种键值对的映射关系存储结构。可以被视为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值,是由二进制表示的零。并且 mapping 没有长度,键集合,值集合的概念。

       constructor:合约的构造函数,是 solidity v0.4.23 后推荐的写的法,原来的首字母大写且与合约名相同来作为构造函数的写法已被废弃。

       emit:solidity 0.4.23 后,调用事件时要加此前缀。

4、部署合约

       前面说过,本项目我们使用 Truffle 框架,可以使用其提供的功能方便的进行开发,测试,部署等工作。这里我们使用它的 migration 功能来部署合约。

       在 migrations 目录下,新建一个名为 2_deploy_contract.js 的文件,并在其中录入如下内容:

var Election = artifacts.require("./Election.sol");
module.exports = function (deployer) {
    deployer.deploy(Election);
};

       现在一切准备就绪,只需要在终端上执行下面的命令,就能部署好了:

truffle migrate --reset

       这个命令会执行所有位于 migrations 目录内的移植脚本,如果之前已经有过成功移植,再次执行 truffle migrate 仅会执行新创建的移植。如果没有新的移植脚本,这个命令不会执行任何操作。而 --reset 就是告诉框架从头执行移植脚本。

       truffle migrate 在执行时会自动调用 truffle compile 进行编译,编译结果以 json 格式存放在 build 目录中。

扫描二维码关注公众号,回复: 1632829 查看本文章

5、编写页面

       本次我们做的是一个投票项目,因此,一定是有页面的,以供用户操作使用。这里我们是在 pet-shop 项目上进行改造,所以只需要去修改 src/index.html 内容就可以了,使用以下代码替换原有内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>区块链投票</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-xs-12 col-sm-8 col-sm-push-2">
            <h1 class="text-center">区块链投票</h1>
            <hr/>
            <br/>
            <div id="loader"><p class="test-center">Loading...</p></div>
            <div id="content" style="display: none;">
                <table>
                    <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">Name</th>
                        <th scope="col">Votes</th>
                    </tr>
                    </thead>
                    <tbody id="candidatesResults"></tbody>
                </table>
                <hr/> <!-- 投票 -->
                <form onsubmit="App.castVote();return false;">
                    <div class="form-group">
                        <label for="cadidatesSelect">选择你要投的名字:</label>
                        <select class="form-control" id="cadidatesSelect"> </select>
                    </div>
                    <button type="submit" class="btn btn-primary">投票</button>
                </form>
            </div>
            <hr/>
            <p id="accountAddress" class="test-center"></p></div>
    </div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>
<style> .test-center {
    text-align: center;
}

table {
    width: 100%;
}

table tr {
    border-bottom: 2px solid #efefef;
    height: 40px;
}
</style>

       页面十分简单,就是 <table> 绘制表格,<form onsubmit="App.castVote();return false;"> 进行投票。

6、编写 js文件

       js 部分是该 Dapp 比较麻烦的地方,因为其中使用了一个新工具 web3.js,还要和智能合约的事件相结合,这里还是先将代码贴出来,再进行解释:

App = {
    web3Provider: null,
    contracts: {},
    account: '0x0',
    init: function () {
        return App.initWeb3();
    },
    initWeb3: function () {
        if (typeof web3 !== 'undefined') {
            App.web3Provider = web3.currentProvider;
            console.warn("Meata");
        } else {
            App.web3Provider = new Web3.providers.HttpProvider('http://localhost:8545/');
        }
        web3 = new Web3(App.web3Provider);
        return App.initContract();
    },
    initContract: function () {
        $.getJSON("Election.json", function (election) {
            App.contracts.Election = TruffleContract(election);
            App.contracts.Election.setProvider(App.web3Provider);
            App.listenForEvents();
            return App.reander();
        })
    },
    reander: function () {
        var electionInstance;
        var $loader = $("#loader");
        var $content = $("#content");
        $loader.show();
        $content.hide();
        // 获得账号信息 
        web3.eth.getCoinbase(function (err, account) {
            if (err === null) {
                App.account = account;
                $("#accountAddress").html("您当前的账号: " + account);
            }
        });
        // 加载数据 
        App.contracts.Election.deployed().then(function (instance) {
            electionInstance = instance;
            return electionInstance.candidateCount();
        }).then(function (candidatesCount) {
            var $candidatesResults = $("#candidatesResults");
            $candidatesResults.empty();
            var $cadidatesSelect = $("#cadidatesSelect");
            $cadidatesSelect.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];
                    var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>";
                    $candidatesResults.append(candidateTemplate);
                    // 投票 
                    var cadidateOption = "<option value='" + id + "'>" + name + "</option>";
                    $cadidatesSelect.append(cadidateOption);
                });
            }
            return electionInstance.voters(App.account);
        }).then(function (hasVoted) {
            if (hasVoted) {
                $('form').hide();
            }
            $loader.hide();
            $content.show();
        }).catch(function (err) {
            console.warn(err);
        });
    },
    // 投票 
    castVote: function () {
        var $loader = $("#loader");
        var $content = $("#content");
        var candidateId = $('#cadidatesSelect').val();
        App.contracts.Election.deployed().then(function (instance) {
            return instance.vote(candidateId, {from: App.account});
        }).then(function (result) {
            $content.hide();
            $loader.show();
        }).catch(function (err) {
            console.warn(err);
        });
    },
    // 监听事件 
    listenForEvents: function () {
        App.contracts.Election.deployed().then(function (instance) {
            instance.votedEvent({}, {formBlock: 0, toBlock: 'latest'}).watch(function (error, event) {
                console.log("event triggered", event);
                App.reander();
            });
        })
    }
};
$(function () {
    $(window).load(function () {
        App.init();
    });
});

       方法 initWeb3,是对 web3.js 进行初始化。

       initContract 方法中,先通过 getJSON 读取本地 json 格式的合约文件,再调用 Truffle 的 TruffleContract 方法进行合约初始化,之后通过 setProvider 为 js 中的 合约设置代理,即,之后会通过 web3 的 httpProvider 执行真实的合约调用。

       App.contracts.Election.deployed().then(function (instance) 是如果合约已经实例化了,则调用 then 中的方法,之后再通过 ES6 的链式语法执行后面的 then 方法。

7、运行项目

       完成上面的代码后,一切准备就绪,现在需要运行起来:

npm run dev

       该命令会执行前端项目的编译,并启动一个轻量的 http 服务器,这需要消耗一点时间,之后会自动打开浏览器,并跳转到项目的地址,默认端口是 3000。若使用 chrome 浏览器,此时会停在 “loading” 界面,接下来我们配置 MetaMask,一款浏览器插件钱包。

8、安装 MetaMask

       打开 chrome 浏览器,地址栏输入 https://chrome.google.com/webstore/category/extensions,在出现的页面中搜索 metamask 并安装插件。

安装MetaMask插件

       安装成功后,浏览器右侧会显示 “小狐狸” 图标,如未显示出来,可将 chrome 地址栏的右边界向左拉。

MetaMask小狐狸

       点击小狐狸图标,切换到本地私有网络:

MetaMask本地私有网络

       再次点击 小狐狸,首先是提示界面,点击“Accept”,进入下一步,下一步也是声明,需要拉到底部才能点击“Accept”。

MetaMask接受声明

       然后会看到此界面,请输入两次密码一定不能忘记,当然如果只是用作开发,就没关系,我们可以随时创建新的。

MetaMask创建地址

       在创建账号的时候为了防止账号密码丢失,这里提供用于找回的助记词功能,一共是12个单词,切记,这一步很重要,一定要把这安全码记录下来方便恢复账号。

MetaMask助记词

       然后系统会生成一个以太坊的地址,并进入钱包页面。

MetaMask钱包页面

9、进行投票

       登录钱包后,刷新投票页面,此时可以正确显示投票页面了,可以看到目前所有候选人票数都为 “0”,点击 “投票”,会 MetaMask 钱包页面进行支付,这时会提示我们钱包余额不足,且我们账户的余额为 “0”。

投票钱包支付

       这里是对合约进行投票,又不是做以太币转移,为什么会提示余额不足呢。结合前面的章节,稍作思考可以知道,对合约的投票行为属于以太坊交易,虽然不需要转移以太币,但以太坊上任何交易都需要花费手续费 gas,而此时我们钱包为零,所以当然会提示余额不足。

       接下来我们在私链上为自己的钱包充值。点击 MetaMask 支付界面 Account 1 账户下面的地址进行拷贝,打开 Ethereum Wallet 钱包,并连接到我们的私链。

  • Mac 下:
/Applications/Ethereum\ Wallet.app/Contents/MacOS/Ethereum\ Wallet --rpc http://localhost:8545/

       点击 Send 按钮,在 to 中添入刚刚拷贝的 MetaMask 钱包账户地址,From 随意选择一个有余额的账户,Amount 输入 10,最后点击页面最底部的 SEND 按钮。

向MetaMask支付以太币

       Ethereum Wallet 钱包会提示输入 From 账户的密码以进行解锁并支付,由于目前是在 Ganache-cli 私链上,它的设计是方便用于开发,而非用做生产网络,因此这里可以不输入密码,直接点击 SEND TRANSACTION。

解锁From账户

       再次回到投票页面,点击 “投票”,可以看到此时账户中已有 10ETH 余额,且不会再提示余额不足,点击 “Submit” 确认提交我们的投票交易。

MetaMask提交交易

       提交后页面会自动刷新,可以看到候选人已经有了一张选票。

投票成功

       想直接看效果的小伙伴可以直接下载笔者的代码:Gitee 用区块链投票

猜你喜欢

转载自blog.csdn.net/xuguangyuansh/article/details/80308275
今日推荐