Score Chain - a simple Dapp

智能合约部署和客户端实现

Posted by Nino Lau on December 17, 2018

根据去中心化的课程评分系统白皮书,我们草拟了评分系统合约,实现了打分可追溯和打分次数记录等基本功能,并将其部署在了私有链上。此外,初步实现了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

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

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 对应:

部署合约到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变少了,说明部署合约确实有以太币的花费!

这时候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

五个测试都通过了!!!


合约客户端

接口设计

初始化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就派上用场了!

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

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

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

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

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

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

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

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

系统说明

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


参考资料

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