实验六:简单Dapp 的开发

实验概述

DApp(Decentralized Application)去中心化应用,自 P2P 网络出现以来就已经存在,是一种运行在计算机 P2P 网络而不是单个计算机上的应用程序。DApp 以一种不受任何单个实体控制的方式存在于互联网中。在区块链技术产生之前,BitTorrent,Popcorn Time,BitMessage等都是运行在P2P网络上的DApp,随着区块链技术的产生和发展,DApp 有了全新的载体和更广阔的发展前景。

DApp 应具备代码开源、激励机制、非中心化共识和无单点故障四个要素,而最为基本的 DApp 结构即为前端+智能合约形式。本实验以以太坊为基础,首先用 Solidity 语言编写实现会议报名登记功能的智能合约,加深编写智能合约的能力;之后学习以太坊私有链的搭建、以及合约在私有链上的部署,从而脱离 Remix,学习更为强大的 Truffle 开发组件;

进而学习 web3.js 库,实现前端对智能合约的调用,构建完整的 DApp;最后可对该DApp 加入个性化机制,例如加入 Token 机制等,作为实验选做项。该实验实现了一个简单的 DApp,但包含了 DApp 开发的必备流程,为将来在以太坊上进行应用开发打下了基础。

预备知识

Solidity 与智能合约

  • 请参照实验五

Truffle 组件

Web3.js

  • web3.js 是一个 JavaScript 库

MetaMask

  • MetaMask 是一个开源的以太坊钱包,以浏览器插件的形式运行,用户能够方便地在浏览器中通过该插件连接到以太坊网络中,控制自己的账号进行交易。

实验内容

实验 6-1 会议报名登记系统的基本功能与实现

  • Enrollment.sol

    • 前置准备
      1
      2
      // 受托者到其委托人信息的映射
      mapping (address => Participant[]) trustees;
    • 实现委托函数delegate
      1
      2
      3
      4
      function delegate(address addr) public returns(address){
      trustees[addr].push(participants[msg.sender]);
      return addr;
      }
    • 实现为委托者报名函数enrollfor
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      function enrollFor(string memory username, string memory title) public returns(string memory){
      bool flag = false;
      uint index = 0;
      for (uint i = 0; i < trustees[msg.sender].length; i++) {
      if (keccak256(bytes(trustees[msg.sender][i].name)) == keccak256(bytes(username))) {
      flag = true;
      index = i;
      break;
      }
      }
      require(flag == true, "Undelegate!");
      for (i = 0; i < conferences.length; i++) {
      if (keccak256(bytes(conferences[i].title)) == keccak256(bytes(title))) {
      require(conferences[i].current < conferences[i].max, "Enrolled full!");
      conferences[i].current = conferences[i].current + 1;
      if (conferences[i].current == conferences[i].max) {
      emit ConferenceExpire(title);
      }
      participants[usernameToAddress[trustees[msg.sender][index].name]].confs.push(title);
      participants[usernameToAddress[trustees[msg.sender][index].name]].signIn = false;
      }
      }
      uint len = participants[usernameToAddress[trustees[msg.sender][index].name]].confs.length;
      require(len > 0, "Conference does not exist!");
      return
      participants[usernameToAddress[trustees[msg.sender][index].name]].confs[len - 1];
      }
  • 练习 6-1:

  1. 应在合约的哪个函数指定管理员身份?如何指定?

    • 需要在合约的构造函数处指定,根据实际情景,应该是创建这个会议体制的人为管理员,所以指定admin为msg.sender。
  2. 在发起新会议时,如何确定发起者是否为管理员?简述 require()、assert()、revert()的区别。

    • require和assert都是条件声明,这三种方式的执行逻辑是相同的:
      1
      2
      3
      4
      5
      require(msg.sender == admin, "permission denied!")
      assert(msg.sender == admin, "permission denied!")
      if(msg.sender != admin){
      revert("permission denied!");
      }
    • 区别在于revert和require在执行失败后会返还gas,但是assert就算执行失败了也会照样扣除gas。
  3. 简述合约中用 memory 和 storage 声明变量的区别。

    • Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
    • 一般情况下不用管这两个关键字, 默认情况下Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
    • 然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的结构体数组 时。这两个属性主要用于*

      *优化代码**以节省合约的gas消耗,故有时需要显式地声明 storage 或 memory 。

实验 6-2 学习用 Truffle 组件部署和测试合约

  • 安装 Truffle 和 Ganache

    • Truffle:
      1
      2
      npm install truffle -g
      truffle -v

      image-20231101195326931

    • Ganache:
      官网安装图形化界面

      image-20231101195249722

  • 新建 truffle 项目并导入合约

    • 初始化项目
      1
      truffle init myDapp

      2-init

      目录结构如下:(未含有与指导书一致的Migration.sol等文件,猜测是版本差异,故手动copy进目录)

      2-tree

    • Enrollment.sol 放入 contracts 文件夹,编写migrations 中的部署脚本 2_deploy_contracts.js
      (需要将ConvertLib相关注释掉,并没有这个库,后续部署会报错)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 2_deploy_contracts.js
      // const ConvertLib = artifacts.require("ConvertLib");
      const Enrollment = artifacts.require("Enrollment");

      module.exports = function(deployer) {
      // deployer.deploy(ConvertLib);
      // deployer.link(ConvertLib, Enrollment);
      deployer.deploy(Enrollment);
      };
  • 配置 truffle-config.js,用于之后配置 Ganache

    • 配置network
      1
      2
      3
      4
      5
      6
      7
      8
      9
      networks: {
      development: {
      host: "127.0.0.1", // Localhost (default: none)
      port: 7545, // Standard Ethereum port (default: none)
      // port: 8545, // Standard Ethereum port (default: none)
      network_id: "*", // Any network (default: none)
      },
      }

    • 配置compiler版本为0.4.26
      1
      2
      3
      4
      5
      compilers: {
      solc: {
      version: "0.4.26", // Fetch exact version from solc-bin (default: truffle's version)
      },
      },
  • 为合约编写测试文件

    • 补充测试enroll函数,确保当用户报名后,其已报名会议列表中有该会议。提示:不要忘记先由管理员创建会议。
      1
      2
      3
      4
      5
      6
      function testEnroll() public{
      Enrollment test = new Enrollment();
      test.newConference("conf2", "beijing", 30);
      string memory expected = "conf2";
      Assert.equal(test.enroll("conf2"), expected, "enroll failed");
      }
  • 用 Ganache 搭建私链

    • 导入truffle-config.js,搭建私链

      image-20231101202913789

    • 可以看到十个账户等

      image-20231101203022363

  • 对合约进行测试和部署

    • 进行测试
    1
    truffle test

    2-test

  • 部署合约
    1
    truffle migrate

    image-20231101203458633

  • 练习 6-2:观察合约的部署过程:

    • 请观察部署完成后 Ganache 的 Blocks、Transactions 以及 Logs 记录,完整叙述合约的部署流程以及合约调用。
      1. Blocks

        image-20231101203610964

      2. Transactions

        image-20231101203714947

      3. Logs

        image-20231101203741774

      部署之前会先进行链的初始化,会在本地保存到一个json文件中。之后需要进行合约 的编写,进行详细的合约规定。第三步需要对合约进行编译,将之前的json文件中的 ABI和EVM code进行获取编译。第四步实现合约的部署,将已经实现好的合约部署 到网络。

      实验 6-3 利用 Web3.js 实现合约与前端的结合

      • 前端界面接口

        • delegate/index.js
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          const mapDispatchToProps = (dispatch) => {
          return {
          submit(address) {
          contract.methods.delegate(address) //输入参数
          .send({from: window.web3.eth.accounts[0]}, function (err, res) {
          console.log(res)
          }) //function中的res为方法返回值
          .then(); //该res为交易执行完后的具体交易信息,如TxHash等
          dispatch({
          type: 'submit_delegate'
          })
          },
          handleChange(e) {
          dispatch({
          type: 'address',
          value: e.target.value
          })
          },
          }
          }
        • Enroll for/index.js
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          const mapDispatchToProps = (dispatch) => {
          return {
          submit(username, title) {
          contract.methods.enrollFor(username, title) //输入参数
          .send({from: window.web3.eth.accounts[0]}, function (err, res) {
          console.log(res)
          }) //function中的res为方法返回值
          .then(); //该res为交易执行完后的具体交易信息,如TxHash等
          dispatch({
          type: 'submit_enrollfor'
          })
          },
          handleChange(e) {
          if (e.target.placeholder === 'Title of Conference')
          dispatch({
          type: 'enrollfor_title',
          value: e.target.value
          })
          else
          dispatch({
          type: 'enrollfor_username',
          value: e.target.value
          })
          },
          }
          }
        • Myconf/index.js
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          componentDidMount() {
          //先执行一遍查询操作
          contract.methods.queryMyConf()
          .call({from: window.web3.eth.accounts[0]}, (err, res) => {
          //将返回的数组依次压入data中
          this.setState({loading: true});
          for (let i = 0; i < res.length; i = i + 1) {
          data.push({conf: res[i]});
          }
          })
          .then(() => {
          //更新状态,使页面数据重新渲染
          this.setState({loading: false});
          });
          contract.events.MyNewConference({
          filter: {},
          fromBlock: window.web3.eth.getBlockNumber()
          }, (error, event) => {
          this.setState({loading: true});
          data.push({conf: event.returnValues[0]});
          this.setState({loading: false});
          })
          }
      • 在前端项目文件中配置合约信息

        • 在 src/contracts/contract.js 文件中的标注位置粘贴ABI

          image-20231101205452575

        • 在 Ganache 的 contracts 中找到部署的 Enrollment 合约地址,同样粘贴到 contract.js 的标注位置

          image-20231101205558523

          image-20231101205616470

    • 问题:表单类组件,通过 contract.methods 进行调用即可,调用应采用 call()方法还是 send()方法?

      • 应该使用call()方法
        • call()将调用“常量”方法并在 EVM 中执行其智能合约方法,而不发送任何交易。注意调用不能改变智能合约状态。
        • send()将向智能合约发送交易并执行其方法。请注意,这可能会改变智能合约状态。

实验测试

  • Metamask安装后连接私链

    image-20231101210802111

  • 启动前端

    1
    npm install

    image-20231101210453093

    注意在package.json中需要加入如下命令,否则会产生报错

    image-20231101210952810

  • 浏览器通过Metamask与私链进行连接

    image-20231101210920473

  • 成功加载界面如下

    image-20231101211303305

    • 功能测试

      (注:期间操作均会触发metamask如下确认界面,方便测试省去截图)

      image-20231101212205630

      • 管理员(用户1)注册

        image-20231101211812407

      • 用户1创建两个新的会议,可以看到展示在会议列表中

        image-20231101212023216image-20231101212116581

      • 用户1报名会议1,可以发现会议1出现在用户1的会议列表中

        image-20231101212141855image-20231101212419808

      • 通过ganache中私钥导入用户2,metamask切换至用户2

        image-20231101212441091image-20231101212459869

      • 用户2注册,且可以发现用户2的会议列表为空

        image-20231101212557183

      • 用户2尝试创建新会议,metamask在确认之前提醒错误

        image-20231101212659369

      • 用户2委托用户1(使用用户1地址)

        image-20231101212818164

        image-20231101212833879

      • metamask切换回用户1,并为用户2报名会议2

        image-20231101212926431

      • 切换回用户2,可以发现会议2出现在用户2的会议列表中

        image-20231101213021217

    • 检查metamask交互记录

      • 用户1

        image-20231101213118250

      • 用户2

        image-20231101213047027

  • 检查ganache

    • blocks

      image-20231101213547643

    • transaction

      image-20231101213604746