blockchain-ex6
实验六:简单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
4function 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
27function 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:
-
应在合约的哪个函数指定管理员身份?如何指定?
-
需要在合约的构造函数处指定,根据实际情景,应该是创建这个会议体制的人为管理员,所以指定admin为msg.sender。
-
-
在发起新会议时,如何确定发起者是否为管理员?简述 require()、assert()、revert()的区别。
-
require和assert都是条件声明,这三种方式的执行逻辑是相同的:
1
2
3
4
5require(msg.sender == admin, "permission denied!")
assert(msg.sender == admin, "permission denied!")
if(msg.sender != admin){
revert("permission denied!");
} -
区别在于revert和require在执行失败后会返还gas,但是assert就算执行失败了也会照样扣除gas。
-
-
简述合约中用 memory 和 storage 声明变量的区别。
-
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
-
一般情况下不用管这两个关键字, 默认情况下Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
-
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的结构体和数组 时。这两个属性主要用于*
*优化代码**以节省合约的gas消耗,故有时需要显式地声明 storage 或 memory 。
-
实验 6-2 学习用 Truffle 组件部署和测试合约
-
安装 Truffle 和 Ganache
-
Truffle:
1
2npm install truffle -g
truffle -v -
Ganache:
官网安装图形化界面
-
-
新建 truffle 项目并导入合约
-
初始化项目
1
truffle init myDapp
目录结构如下:(未含有与指导书一致的Migration.sol等文件,猜测是版本差异,故手动copy进目录)
-
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
9networks: {
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
5compilers: {
solc: {
version: "0.4.26", // Fetch exact version from solc-bin (default: truffle's version)
},
},
-
-
为合约编写测试文件
-
补充测试enroll函数,确保当用户报名后,其已报名会议列表中有该会议。提示:不要忘记先由管理员创建会议。
1
2
3
4
5
6function 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,搭建私链
-
可以看到十个账户等
-
-
对合约进行测试和部署
-
进行测试
1
truffle test
-
-
部署合约
1
truffle migrate
-
练习 6-2:观察合约的部署过程:
-
请观察部署完成后 Ganache 的 Blocks、Transactions 以及 Logs 记录,完整叙述合约的部署流程以及合约调用。
-
Blocks
-
Transactions
-
Logs
部署之前会先进行链的初始化,会在本地保存到一个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
20const 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
26const 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
23componentDidMount() {
//先执行一遍查询操作
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
-
在 Ganache 的 contracts 中找到部署的 Enrollment 合约地址,同样粘贴到 contract.js 的标注位置
-
-
-
问题:表单类组件,通过 contract.methods 进行调用即可,调用应采用 call()方法还是 send()方法?
-
应该使用call()方法
-
call()将调用“常量”方法并在 EVM 中执行其智能合约方法,而不发送任何交易。注意调用不能改变智能合约状态。
-
send()将向智能合约发送交易并执行其方法。请注意,这可能会改变智能合约状态。
-
-
-
实验测试
-
Metamask安装后连接私链
-
启动前端
1
npm install
注意在package.json中需要加入如下命令,否则会产生报错
-
浏览器通过Metamask与私链进行连接
-
成功加载界面如下
-
功能测试
(注:期间操作均会触发metamask如下确认界面,方便测试省去截图)
-
管理员(用户1)注册
-
用户1创建两个新的会议,可以看到展示在会议列表中
-
用户1报名会议1,可以发现会议1出现在用户1的会议列表中
-
通过ganache中私钥导入用户2,metamask切换至用户2
-
用户2注册,且可以发现用户2的会议列表为空
-
用户2尝试创建新会议,metamask在确认之前提醒错误
-
用户2委托用户1(使用用户1地址)
-
metamask切换回用户1,并为用户2报名会议2
-
切换回用户2,可以发现会议2出现在用户2的会议列表中
-
-
检查metamask交互记录
-
用户1
-
用户2
-
-
-
检查ganache
-
blocks
-
transaction
-
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 lzhのBLOG!