Score-chain 智能合約的部署和客戶端設(shè)計(jì)

根據(jù)去中心化的課程評(píng)分系統(tǒng)白皮書(shū),我們草擬了評(píng)分系統(tǒng)合約,實(shí)現(xiàn)了打分可追溯和打分次數(shù)記錄等基本功能,并將其部署在了私有鏈上。此外,初步實(shí)現(xiàn)了DAPP功能,并用 node.js 搭建了頁(yè)面。未來(lái)的計(jì)劃包括:打分合約的撰寫(xiě)和部署,以及打分界面的設(shè)計(jì)和優(yōu)化。


實(shí)驗(yàn)依賴(lài)

Node.js

節(jié)點(diǎn)包管理器(NPM)在本次實(shí)驗(yàn)中作為web開(kāi)發(fā)的工具。家喻戶曉的npm這里就不再贅述了,用homebrew安裝node.js:

brew install node 

顯示版本成功為安裝好了:

npm -v

Truffle

Truffle??(松露)——“聰明的合同更甜蜜”,是一個(gè)簡(jiǎn)潔的智能合約開(kāi)發(fā)框架。通過(guò)下載demo,我們可以很快的上手部署合約,并且Truffle最讓人驚嘆之處在于它甚至提供了和node.js 協(xié)同開(kāi)發(fā)web Ui的interface,真的因此十分符合初級(jí)Dapper的需求。

用node.js安裝Truffle

npm install -g truffle

Ganache

Truffle Suite提供了一個(gè)很好用的私有鏈工具——Ganache。Ganache可以快速啟動(dòng)個(gè)人Ethereum區(qū)塊鏈,可以使用它來(lái)運(yùn)行測(cè)試、執(zhí)行命令和檢查狀態(tài),同時(shí)控制鏈的操作方式。這里用Ganache就是為了方便創(chuàng)建accounts。

Ganache可以從這里下載。

Metamask

”小狐貍??“——Metamask 是 Google Chrome 瀏覽器的擴(kuò)展,將以太坊與 Google Chrome 結(jié)合,在 Chrome 瀏覽器上運(yùn)行以太坊 DApps,以及身份識(shí)別的工具。于是,它就具備了類(lèi)似 Mist 的錢(qián)包功能,允許用戶管理自己的賬戶,通過(guò) Web3 JavaScript API,讓 DApp 與以太坊區(qū)塊鏈實(shí)現(xiàn)交互。從Chrome Extension Store里就能下載Metamask,注冊(cè)一個(gè)賬戶就能用了(雖然沒(méi)錢(qián)??)。當(dāng)然,我們之后用到的賬戶并不是這個(gè)注冊(cè)的賬戶,而是Ganache上的賬戶。在Chrome上注冊(cè)賬戶并登陸。


智能合約

Truffle框架

創(chuàng)建項(xiàng)目——Score Chain

mkdir scorechain
cd scorechain

使用[truffleframework.com/boxes/][truffleframework.com/boxes/]快速啟動(dòng)和運(yùn)行。安裝寵物商店demo:

truffle unbox pet-shop

當(dāng)我們的框架安裝好了的時(shí)候,目錄結(jié)構(gòu)如圖:

image

truffle.js 是truffle框架和ganache網(wǎng)絡(luò)連接的配置文件,host一般用localhost,端口取決于ganache的RPC 服務(wù)器如果端口錯(cuò)了后期設(shè)計(jì)網(wǎng)頁(yè)會(huì)一直 loading)。文件內(nèi)容為:

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

src是網(wǎng)頁(yè)開(kāi)發(fā)源代碼目錄,暫時(shí)只用到了app.jsindex.html 這個(gè)文件。app.js這個(gè)文件是前端和后端的interface,是整個(gè)Dapp十分重要的一部分。**index.html **是前端設(shè)計(jì)文件。

node_modules 是node.js的模塊,暫時(shí)不需要。

contracts 就是合約目錄啦!之后我們的合約就是在這里完成的。合約寫(xiě)好了之后需要部署在我們用Ganache創(chuàng)建的私有鏈上,需要在migrations文件夾下寫(xiě)部署文件。當(dāng)部署在私有鏈上的時(shí)候,truffle框架會(huì)為我們生成build,編譯我們的合約。

合約擬寫(xiě)

在contracts目錄下,建立合約文件,用>=0.4.20 <0.6.1 的 solidity編寫(xiě)合約Score(打分)。

構(gòu)建學(xué)生結(jié)構(gòu)體:

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

建立關(guān)于學(xué)生和TA的映射,并創(chuàng)建變量被評(píng)次數(shù):

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

定義添加學(xué)生和TA打分函數(shù):

    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);
    }

最后還需要定義一個(gè)選擇學(xué)生事件:

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

至此,合約擬寫(xiě)成功!

合約部署

為了將合約部署到Ganache私有鏈上,還需要一個(gè)部署文件,在migrations目錄下部署合約:

var Score = artifacts.require("./Score.sol");

module.exports = function(deployer) {
  deployer.deploy(Score);
};

打開(kāi)Ganache,看看RPC 服務(wù)器(7545)是否和truffle.js 對(duì)應(yīng):

image

部署合約到Ganache私有鏈:

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

出現(xiàn)下圖為成功:

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...

打開(kāi)truffle console,聲明合約實(shí)例,檢查我們部署的合約:

$ truffle console // 進(jìn)入console

聲明一個(gè)實(shí)例:

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

看看我們有多少個(gè)學(xué)生(6個(gè)):

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

這就說(shuō)明部署成功了,我們?cè)倏纯茨J(rèn)的部署用戶,Ganache的用戶0:嗯,果然它從原來(lái)的100eth變少了,說(shuō)明部署合約確實(shí)有以太幣的花費(fèi)!

image

這時(shí)候truffle框架為我們自動(dòng)生成build/contracts,目錄下的Score.json是合約的可執(zhí)行文件。之后會(huì)在我們的客戶端開(kāi)發(fā)中用到。

測(cè)試文件

為了驗(yàn)證我們部署的合約是否正確,還需要設(shè)計(jì)幾個(gè)測(cè)試:

touch ./test/score.js

score.js是我們的測(cè)試文件。打開(kāi)文件,設(shè)計(jì)測(cè)試函數(shù)如下:

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() {};

查看測(cè)試結(jié)果:

truffle test

五個(gè)測(cè)試都通過(guò)了?。?!

image

合約客戶端

接口設(shè)計(jì)

初始化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個(gè)學(xué)生的信息;然后加載了合約的內(nèi)容,包括可選學(xué)生和目前學(xué)生的評(píng)分情況。

  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);
    });
  },

最后定義了事件的發(fā)生:

  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);
    });
  }
};

前端設(shè)計(jì)

最后我們還設(shè)計(jì)了一個(gè)和諧友好的前端:

<!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 運(yùn)行我們的客戶端:

npm run dev

一直loading,這時(shí)候我們的小狐貍??——MetaMask就派上用場(chǎng)了!

image

我們用的是主以太坊網(wǎng)絡(luò),應(yīng)該用Ganache定義的私有鏈端口:

image

點(diǎn)擊Costume RPC設(shè)置http://127.0.01:7545,重新加載,頁(yè)面顯示正常:

image

但是我們注冊(cè)的賬戶是沒(méi)錢(qián)的,無(wú)法應(yīng)用合約,打開(kāi)Ganache私有鏈中的一個(gè)賬戶,復(fù)制私鑰??,在Metamask上導(dǎo)入新賬戶:

image

我們?yōu)镹ino進(jìn)行一次打分:選擇Nino,系統(tǒng)彈出一個(gè)標(biāo)簽,用來(lái)確認(rèn)交易。

image

玄學(xué)問(wèn)題:這個(gè)過(guò)程可能有時(shí)候會(huì)發(fā)生錯(cuò)誤,基本上是RPC網(wǎng)絡(luò)連接不佳、私有鏈連接不暢造成的,重啟端口或者更換一個(gè)賬號(hào)打分即可。*

為Nino成功打分,可以看見(jiàn),Select選擇框和按鍵沒(méi)有了(目前規(guī)定一個(gè)賬戶不能重復(fù)打分):

image

TA4打分是需要花錢(qián)的,因此可以看見(jiàn)錢(qián)變少了:

image

再用其他賬戶給學(xué)生們打分吧!

image

系統(tǒng)說(shuō)明

因?yàn)镽PC連接不穩(wěn)定,經(jīng)常會(huì)出現(xiàn)報(bào)錯(cuò):tx的nounce不正確,因此需要頻繁地更換賬戶,這個(gè)問(wèn)題影響了系統(tǒng)的實(shí)用性。另外這個(gè)系統(tǒng)暫時(shí)不允許同一個(gè)賬戶多次打分,為了確保每一次打分都可以被清晰地追溯。


參考資料

吃水不忘挖井人,在此感謝給我?guī)?lái)幫助的重要參考:


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容