随笔 - 18  文章 - 0  评论 - 2  阅读 - 6793

简单Dapp的开发

简单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 开发的必备流程,为将来在以太坊上进行应用开发打下了基础。
实验内容概述如下:
A. 编写实现会议报名功能的智能合约(发起会议,注册,报名会议,委托报名,事件触发)
B. 利用 Truffle 套件将合约部署到以太坊私有链(私有链搭建,合约部署,合约测试)
C. 利用 web3.js 实现前端对合约的调用(账户绑定、合约 ABI、RPC 调用)

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

完成合约文件Enrollment.sol的编写,实现一个报名系统,系统应具有以下功能:

合约参与方包含一个管理员以及其余参与者,管理员可以发起不止一个会议,并指定会议信息以及总人数。参与者首先需要进行注册,将个人基本信息与以太坊地址相关联,并存储在合约上,之后可进行报名,或委托他人为自己报名。当会议报名人满时,该会议将不再可报名。当合约内某些数据发生变化时,应能够触发事件(event)使前端重新获取并渲染数据,例如当某个会议报名人满时,应触发相应事件使前端及时更新可报名会议列表。

实现委托函数及为受托者报名函数
function delegate(address addr) public{
	trustees[addr].push(participants[msg.sender]);
}
function enrollFor(string memory username,string memory title) public returns(string memory){
	uint index = 0;
	for (uint i = 0; i < trustees[msg.sender].length; i++) {
		if (keccak256(bytes(trustees[msg.sender][i].name)) == keccak256(bytes(username))) {
			index = i;
			break;
		}
	}
	for (uint 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 += 1;
			if(conferences[i].current==conferences[i].max){
				emit ConferenceExpire(title);
			}
			trustees[msg.sender][index].confs.push(title);
		}
	}
	uint len = trustees[msg.sender][index].confs.length;
	require(len>0,"Conference does not exist");
	return trustees[msg.sender][index].confs[len-1];
}
练习6-1

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

答:在合约的构造函数中制定管理员的身份。通常将创建会议,也就是部署合约的人设置为管理员,所以在此处指定msg.sender设置为管理员。

2)在发起新会议时,如何确定发起者是否为管理员?简述 require()、assert()、revert()的区别。

答:在上述合约中,通过语句require(msg.sender==admin,"permission denied")来检查发起者是否为管理员。在该语句中,使用require()来检查当前交易的发起者是否为合约的管理员,如果不是,则出发一个异常,终止函数执行,并返回错误消息"permission denied"。

require()、assert()、revert()的区别:当语句执行失败时,require()和revert()会返还相应的gas,而assert()则不会返还相应的gas。

3)简述合约中用 memory 和 storage 声明变量的区别。

答:memory:memory 用于在函数内部临时存储数据;变量在 memory 中声明时,它们的值只在函数的执行期间存储,函数执行完毕后,memory 中的数据将被清除;通常用于存储函数调用中的参数或局部变量,临时操作数据。

storage:storage 用于永久性存储数据,通常用于状态变量;变量在 storage 中声明时,其值将永久保存在以太坊的区块链上,并且可以在不同的交易和函数调用之间保持一致;通常用于存储合约的状态,如合约的状态变量或持久化数据。

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

利用 truffle 组件对实验 6-1 的合约进行测试并部署到本地私链上。利用 truffle 初始化一个以太坊项目,将合约Enrollment.sol 放入 contracts 文件夹,并编写部署名为 2_deploy_contracts.js 的脚本。然后为合约编写测试文件。利用 Ganache 搭建私链后,在终端进入 truffle 项目 lab8 的目录,输入 truffle test 进行测试。测试完成后在终端输入 truffle migrate 进行合约编译和部署。

6-2.2 新建 truffle 项目并导入合约

初始化一个以太坊项目,并导入相关合约文件。

2_deploy_contracts.js

const Enrollment = artifacts.require("Enrollment");

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

truffle-config.js

module.exports = {
  networks: {
    development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 7545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
    },
  },
};

6-2.3 为合约编写测试文件

TestEnrollment.sol

    function testEnroll() public{
        Enrollment test=new Enrollment();
        string memory expected="conf1";
        test.newConference("conf1","beijing",30);
        Assert.equal(test.enroll("conf1"),expected,"Enroll failed");
    }
6-2.4 Ganache 搭建私链

打开 Ganache 客户端,选择 NEW WORKSPACE,在 ADD PROJECT 处导入上一步配置的 truffle-config.js 文件。此时将会在本地运行一个以太坊私链,并创建 10 个账户供使用。

image

6-2.5 对合约进行测试和部署

在终端进入 truffle 项目 lab8 的目录,输入“truffle test”,输出结果如下:

image

使用 Ganache重新搭建一条私链,然后在终端输入“truffle migrate”完成合约的编译和部署。控制台会输出各合约的部署情况以及总共的 gas 花费,Ganache 中的第一个账户即为该合约的部署者。

image

image

image

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

观察部署完成后 Ganache 的 Blocks、Transactions 以及 Logs 记录,记录如下:

image

image

image

详细查看每条交易的信息:

image

image

image

image

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

答:合约的部署流程:首先对链进行的初始化,然后进行合约的编写,进行详细的合约规定。接下来对合约进行编译,将其转换为字节码。然后选择区块链平台,创建钱包和账户。最后进行合约的部署,将编译后的合约部署到网络。当区块链网络确认合同已被添加到区块链中时,交易成功。

合约的调用流程:首先创建交易,该交易包括调用智能合约的数据。接下来对交易进行签名并将交易广播到区块链网络。接下来等待交易被确认,一但确认,合约中的操作将被执行。

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

通过Web3.js实现智能合约和前端之间的调用和订阅:通过前端对以太坊节点进行 RPC 调用,执行合约中的函数,并将合约返回的数据,以及订阅的合约事件提醒及时展示在前端界面。按照实例更改相应组件下的 js 文件以实现对组件的交互,将前端项目中的 src/contracts/contract.js 文件中的 ABI 位置代码更改为合约编译后自动生成的 ABI 信息。

6-3.1 前端界面接口 & 6-3.3 通过 Web3.js 实现前端与合约交互 & 练习 6-3 参考上述注册组件代码中交互代码的写法,完成另外四个表单类组件对合约的调用

对表单中7个文件的index.js代码进行修改和补充,完成对合约的调用,实现合约与前端的结合:

conflist

const mapDispatchToProps = (dispatch) => {
  return {
    submit(title) {
    contract.methods.enroll(title) //输入参数
    .send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)})  //function中的res为方法返回值
    .then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

    dispatch({
        type: 'submit_enroll'
      })
    },

    handleChange(e) {
      dispatch({
        type: 'enroll_title',
        value: e.target.value
      })
    },
  }
}

delegate

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)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

      dispatch({
        type: 'submit_delegate'
      })
    },

    handleChange(e) {
      dispatch({
        type: 'address',
        value: e.target.value
      })
    },
  }
}

enroll

const mapDispatchToProps = (dispatch) => {
  return {
    submit(title) {
    contract.methods.enroll(title) //输入参数
    .send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)})  //function中的res为方法返回值
    .then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

    dispatch({
        type: 'submit_enroll'
      })
    },

    handleChange(e) {
      dispatch({
        type: 'enroll_title',
        value: e.target.value
      })
    },
  }
}

enrollfor

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)=>console.log(res)); //该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

componentDidMount(){
    //先执行一遍查询操作
    contract.methods.queryMyConf()
    .call({from:window.web3.eth.accounts[0]},(err,res)=>{
      //将返回的数组依次压入data中
      this.setState({loading: true});
      if(res != null){
        for(var i=0;i<res.length;i=i+1){
          data.push({title:res[i]});
        }
    }
      else{
        data.push({'title': 'no'});
    }
  })
    .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});
     })
}

newconf

const mapDispatchToProps = (dispatch) => {
  return {
    submit(title,detail,limitation) {
      contract.methods.newConference(title,detail,limitation) //输入参数
      .send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)})  //function中的res为方法返回值
      .then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

      dispatch({
        type: 'submit_newconf'
      })
    },

    handleChange(e) {
      switch (e.target.placeholder) {
        case 'Title':
          dispatch({
            type: 'newconf_title',
            value: e.target.value
          })
          break;
        case 'Detail':
          dispatch({
            type: 'detail',
            value: e.target.value
          })
          break;
        case 'Limitation':
          dispatch({
            type: 'limitation',
            value: e.target.value
          })
          break;
        default:
          break;
      }
    },
  }
}

signup

const mapDispatchToProps = (dispatch) => {
  return {
    submit(username,extra) {
      contract.methods.signUp(username,extra) //调用合约signUp方法
      .send({from:window.web3.eth.accounts[0]},function(err,res){console.log(res)})  //function中的res为方法返回值
      .then((res)=>console.log(res)); //该res为交易执行完后的具体交易信息,如TxHash等

      dispatch({
        type: 'submit_signup'
      })
    },

    handleChange(e) {
      if (e.target.placeholder === 'username')
        dispatch({
          type: 'username',
          value: e.target.value
        })
      else
        dispatch({
          type: 'extra',
          value: e.target.value
        })
    },
  }
6-3.2 在前端项目文件中配置合约信息

将/build 文件夹下会生成的json文件中的ABI信息拷贝到contract.js文件中;在 Ganache 的 contracts 中找到部署的 Enrollment 合约,将合约地址复制到contract.js中。

contract.js

import Web3 from 'web3';
//在此粘贴ABI信息
const abi = [
  {
    "inputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "title",
        "type": "string"
      }
    ],
    "name": "ConferenceExpire",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "title",
        "type": "string"
      }
    ],
    "name": "MyNewConference",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "title",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "detail",
        "type": "string"
      }
    ],
    "name": "NewConference",
    "type": "event"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "admin",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "conferences",
    "outputs": [
      {
        "internalType": "string",
        "name": "title",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "detail",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "max",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "current",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "participants",
    "outputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "extra",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "string",
        "name": "username",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "extra",
        "type": "string"
      }
    ],
    "name": "signUp",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "string",
        "name": "title",
        "type": "string"
      }
    ],
    "name": "enroll",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "string",
        "name": "title",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "detail",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "max",
        "type": "uint256"
      }
    ],
    "name": "newConference",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "queryConfList",
    "outputs": [
      {
        "internalType": "string[]",
        "name": "",
        "type": "string[]"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "queryMyConf",
    "outputs": [
      {
        "internalType": "string[]",
        "name": "",
        "type": "string[]"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "address",
        "name": "addr",
        "type": "address"
      }
    ],
    "name": "delegate",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "string",
        "name": "username",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "title",
        "type": "string"
      }
    ],
    "name": "enrollFor",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  }
];

// 在此粘贴合约地址(去掉0x)
const address = 'CFE3031dc731DD36eE88B13d6dbD0794Ba633Eae';

// window.web3.currentProvider为当前浏览器的web3 Provider
const web3 = new Web3(window.web3.currentProvider);

// 允许连接到metamask
window.ethereum.enable();

// 导出合约实例
export default new web3.eth.Contract(abi, address);

部署完成后,在lab8-frontend文件夹中打开文件夹,先执行npm install命令,然后再执行npm start命令。执行之后,成功打开部署的智能合约前端界面,并且能够成功打开MetaMask。

image

在MetaMask中创建一个账户并登录,之后在网页中注册一个用户:

image

可以看到Chrome支付成功的提示:

image

在Ganache中也可以查询到交易记录:image

新建一个会议:

image

支付后在Ganache中查询交易记录:

image

加入刚才创建的会议:

image

支付后在Ganache中也查询该条交易记录:

image

刷新页面后可以查询到会议列表和已加入的会议中包含刚才创建的Block Chain会议。

image

问题:这里的调用,应采用 call()方法还是 send()方法?

对于展示类组件,如可报名会议列表和本人已报名会议,通常应使用 call() 方法来调用合约中的查询类方法。call() 方法用于读取合约的状态,而不会创建交易,因此不需要消耗以太币。上述查询方法仅涉及读取数据,而不涉及修改区块链状态。而 send() 方法用于发送事务,可能涉及到交易费用和修改区块链状态。

posted on   CyberFisher  阅读(592)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
< 2025年3月 >
23 24 25 26 27 28 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 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示