美文网首页
Score-chain 智能合约的部署和客户端设计

Score-chain 智能合约的部署和客户端设计

作者: Nino_Lau | 来源:发表于2018-12-17 18:25 被阅读0次

    根据去中心化的课程评分系统白皮书,我们草拟了评分系统合约,实现了打分可追溯和打分次数记录等基本功能,并将其部署在了私有链上。此外,初步实现了DAPP功能,并用 node.js 搭建了页面。未来的计划包括:打分合约的撰写和部署,以及打分界面的设计和优化。


    实验依赖

    Node.js

    节点包管理器(NPM)在本次实验中作为web开发的工具。家喻户晓的npm这里就不再赘述了,用homebrew安装node.js:

    brew install node 
    

    显示版本成功为安装好了:

    npm -v
    

    Truffle

    Truffle🍰(松露)——“聪明的合同更甜蜜”,是一个简洁的智能合约开发框架。通过下载demo,我们可以很快的上手部署合约,并且Truffle最让人惊叹之处在于它甚至提供了和node.js 协同开发web Ui的interface,真的因此十分符合初级Dapper的需求。

    用node.js安装Truffle

    npm install -g truffle
    

    Ganache

    Truffle Suite提供了一个很好用的私有链工具——Ganache。Ganache可以快速启动个人Ethereum区块链,可以使用它来运行测试、执行命令和检查状态,同时控制链的操作方式。这里用Ganache就是为了方便创建accounts。

    Ganache可以从这里下载。

    Metamask

    ”小狐狸🦊“——Metamask 是 Google Chrome 浏览器的扩展,将以太坊与 Google Chrome 结合,在 Chrome 浏览器上运行以太坊 DApps,以及身份识别的工具。于是,它就具备了类似 Mist 的钱包功能,允许用户管理自己的账户,通过 Web3 JavaScript API,让 DApp 与以太坊区块链实现交互。从Chrome Extension Store里就能下载Metamask,注册一个账户就能用了(虽然没钱💰)。当然,我们之后用到的账户并不是这个注册的账户,而是Ganache上的账户。在Chrome上注册账户并登陆。


    智能合约

    Truffle框架

    创建项目——Score Chain

    mkdir scorechain
    cd scorechain
    

    使用[truffleframework.com/boxes/][truffleframework.com/boxes/]快速启动和运行。安装宠物商店demo:

    truffle unbox pet-shop
    

    当我们的框架安装好了的时候,目录结构如图:

    image

    truffle.js 是truffle框架和ganache网络连接的配置文件,host一般用localhost,端口取决于ganache的RPC 服务器如果端口错了后期设计网页会一直 loading)。文件内容为:

    module.exports = {
      networks: {
        development: {
          host: "127.0.0.1",
          port: 7545,
          network_id: "*" 
        }
      }
    };
    
    

    src是网页开发源代码目录,暂时只用到了app.jsindex.html 这个文件。app.js这个文件是前端和后端的interface,是整个Dapp十分重要的一部分。**index.html **是前端设计文件。

    node_modules 是node.js的模块,暂时不需要。

    contracts 就是合约目录啦!之后我们的合约就是在这里完成的。合约写好了之后需要部署在我们用Ganache创建的私有链上,需要在migrations文件夹下写部署文件。当部署在私有链上的时候,truffle框架会为我们生成build,编译我们的合约。

    合约拟写

    在contracts目录下,建立合约文件,用>=0.4.20 <0.6.1 的 solidity编写合约Score(打分)。

    构建学生结构体:

        struct Student {
            uint id;
            string name;
            uint selectCount;
        }
    

    建立关于学生和TA的映射,并创建变量被评次数:

        // Store TAs
        mapping(address => bool) public TAs;
        // Store Students
        mapping(uint => Student) public students;
        // Store Students Count
        uint public scoredTimes;
    

    定义添加学生和TA打分函数:

        function addStudent (string _name) private {
            scoredTimes ++;
            students[scoredTimes] = Student(scoredTimes, _name, 0);
        }
    
        function select (uint _studentId) public {
            // require that they haven't selected before
            require(!TAs[msg.sender]);
    
            // require a valid student
            require(_studentId > 0 && _studentId <= scoredTimes);
    
            // record that TA has selected
            TAs[msg.sender] = true;
    
            // update student select Count
            students[_studentId].selectCount ++;
    
            // trigger selected event
            selectEvent(_studentId);
        }
    

    最后还需要定义一个选择学生事件:

        // select event
        event selectEvent (
            uint indexed _studentId
        );
    

    至此,合约拟写成功!

    合约部署

    为了将合约部署到Ganache私有链上,还需要一个部署文件,在migrations目录下部署合约:

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

    打开Ganache,看看RPC 服务器(7545)是否和truffle.js 对应:

    image

    部署合约到Ganache私有链:

    truffle migrate --reset // 非首次部署要加reset
    

    出现下图为成功:

    Using network 'development'.
    
    Running migration: 1_initial_migration.js
      Replacing Migrations...
      ... 0x8006a2052e71652571a823fc4a33f5f88ea1bc76972ef08dafbaade016e330ab
      Migrations: 0x64745cba2a428767a9c6518da9bc5752492fec22
    Saving successful migration to network...
      ... 0x42cc99e3517718bb12f89b90f8d86e3e4aad0b91a4a0b28331cf89c817de89c2
    Saving artifacts...
    Running migration: 2_deploy_contracts.js
      Replacing Score...
      ... 0x24a274ae9c7fba6142290ebcf5b966faafbc4852f0b752022627679fb6bc8c08
      Score: 0xebde42adb74d844988238720c4e50feada2f6f2f
    Saving successful migration to network...
      ... 0x7d1aedce77df01a0e77f59ca9f55973fb90ba9cf7d61469f8fcc8dbfb7d1067b
    Saving artifacts...
    

    打开truffle console,声明合约实例,检查我们部署的合约:

    $ truffle console // 进入console
    

    声明一个实例:

    Score.deployed().then(function(instance) { app = instance }) 
    

    看看我们有多少个学生(6个):

    app.studentsNum()
    // 显示 BigNumber { s: 1, e: 0, c: [ 6 ] }
    

    这就说明部署成功了,我们再看看默认的部署用户,Ganache的用户0:嗯,果然它从原来的100eth变少了,说明部署合约确实有以太币的花费!

    image

    这时候truffle框架为我们自动生成build/contracts,目录下的Score.json是合约的可执行文件。之后会在我们的客户端开发中用到。

    测试文件

    为了验证我们部署的合约是否正确,还需要设计几个测试:

    touch ./test/score.js
    

    score.js是我们的测试文件。打开文件,设计测试函数如下:

    it("initializes with six students", function(){...};
    
    it("it initializes the students with the correct values", function() {};
    
    it("allows a TA to cast a select", function() {};
    
    it("throws an exception for invalid students", function() {};
       
    it("throws an exception for double selecting", function() {};
    

    查看测试结果:

    truffle test
    

    五个测试都通过了!!!

    image

    合约客户端

    接口设计

    初始化Web3:

      initWeb3: function() {
        if (typeof web3 !== 'undefined') {
          App.web3Provider = web3.currentProvider;
          web3 = new Web3(web3.currentProvider);
        } else {
          App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
          web3 = new Web3(App.web3Provider);
        }
        return App.initContract();
      },
    

    将合约初始化:

      initContract: function() {
        $.getJSON("Score.json", function(score) {
          App.contracts.Score = TruffleContract(score);
          App.contracts.Score.setProvider(App.web3Provider);
          App.listenForEvents();
          return App.render();
        });
      },
    

    等待合约emit给观察者(也就是我们):

      // Listen for events emitted from the contract
      listenForEvents: function() {
        App.contracts.Score.deployed().then(function(instance) {
          instance.selectedEvent({}, {
            fromBlock: 0,
            toBlock: 'latest'
          }).watch(function(error, event) {
            console.log("event triggered", event)
            App.render();
          });
        });
      },
    

    render就是我们主要接口了:首先加载了6个学生的信息;然后加载了合约的内容,包括可选学生和目前学生的评分情况。

      render: function() {
        var scoreInstance;
        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.Score.deployed().then(function(instance) {
          scoreInstance = instance;
          return scoreInstance.studentsNum();
        }).then(function(studentsNum) {
          var studentsResults = $("#studentsResults");
          studentsResults.empty();
    
          var studentsSelect = $('#studentsSelect');
          studentsSelect.empty();
    
          for (var i = 1; i <= studentsNum; i++) {
            scoreInstance.students(i).then(function(student) {
              var id = student[0];
              var name = student[1];
              var scoredTimes = student[2];
    
              // Render student Result
              var studentTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + scoredTimes + "</td></tr>"
              studentsResults.append(studentTemplate);
    
              // Render student ballot option
              var studentOption = "<option value='" + id + "' >" + name + "</ option>"
              studentsSelect.append(studentOption);
            });
          }
          return scoreInstance.TAs(App.account);
        }).then(function(hasSelected) {
          // Do not allow a user to select
          if(hasSelected) {
            $('form').hide();
          }
          loader.hide();
          content.show();
        }).catch(function(error) {
          console.warn(error);
        });
      },
    

    最后定义了事件的发生:

      castSelect: function() {
        var studentId = $('#studentsSelect').val();
        App.contracts.Score.deployed().then(function(instance) {
          return instance.select(studentId, { from: App.account });
        }).then(function(result) {
          // Wait for selects to update
          $("#content").hide();
          $("#loader").show();
        }).catch(function(err) {
          console.error(err);
        });
      }
    };
    

    前端设计

    最后我们还设计了一个和谐友好的前端:

    <!DOCTYPE html>
    <html lang="en">
    <body background="https://ws3.sinaimg.cn/large/006tNbRwgy1fy9tabq6tsj30u00yqq3p.jpg">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
        <title>Score Chain</title>
    
        <!-- Bootstrap -->
        <link href="css/bootstrap.min.css" rel="stylesheet">
    
        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
          <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
      </head>
      <body>
        <div class="container" style="width: 500px;">
          <div class="row">
            <div class="col-lg-12">
              <h1 class="text-center">Score Chain</h1>
              <hr/>
              <br/>
              <div id="loader">
                <p class="text-center">Loading...</p>
              </div>
              <div id="content" style="display: none;">
                <table class="table">
                  <thead>
                    <tr>
                      <th scope="col">Id</th>
                      <th scope="col">Name</th>
                      <th scope="col">Selects</th>
                    </tr>
                  </thead>
                  <tbody id="studentsResults">
                  </tbody>
                </table>
                <hr/>
                <form onSubmit="App.castSelect(); return false;">
                  <div class="form-group">
                    <label for="studentsSelect">Select Student</label>
                    <select class="form-control" id="studentsSelect">
                    </select>
                  </div>
                  <button type="submit" class="btn btn-primary">Select</button>
                  <hr />
                </form>
                <p id="accountAddress" class="text-center"></p>
              </div>
            </div>
          </div>
        </div>
    
        <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
        <!-- Include all compiled plugins (below), or include individual files as needed -->
        <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>
    
    

    Localhost:3000 运行我们的客户端:

    npm run dev
    

    一直loading,这时候我们的小狐狸🦊——MetaMask就派上用场了!

    image

    我们用的是主以太坊网络,应该用Ganache定义的私有链端口:

    image

    点击Costume RPC设置http://127.0.01:7545,重新加载,页面显示正常:

    image

    但是我们注册的账户是没钱的,无法应用合约,打开Ganache私有链中的一个账户,复制私钥🔑,在Metamask上导入新账户:

    image

    我们为Nino进行一次打分:选择Nino,系统弹出一个标签,用来确认交易。

    image

    玄学问题:这个过程可能有时候会发生错误,基本上是RPC网络连接不佳、私有链连接不畅造成的,重启端口或者更换一个账号打分即可。*

    为Nino成功打分,可以看见,Select选择框和按键没有了(目前规定一个账户不能重复打分):

    image

    TA4打分是需要花钱的,因此可以看见钱变少了:

    image

    再用其他账户给学生们打分吧!

    image

    系统说明

    因为RPC连接不稳定,经常会出现报错:tx的nounce不正确,因此需要频繁地更换账户,这个问题影响了系统的实用性。另外这个系统暂时不允许同一个账户多次打分,为了确保每一次打分都可以被清晰地追溯。


    参考资料

    吃水不忘挖井人,在此感谢给我带来帮助的重要参考:


    相关文章

      网友评论

          本文标题:Score-chain 智能合约的部署和客户端设计

          本文链接:https://www.haomeiwen.com/subject/woickqtx.html