本文旨在引导对 DApp 开发感兴趣的开发者, 构建一个基于以太坊去中心化应用, 通过开发一款功能完备的竞猜游戏, 迈出 DApp 开发的第一步, 通过实例讲解 Solidity 语言的常用语法, 以及前端如何与智能合约进行交互.
如果正在阅读的你, 从未接触过 DApp 开发也不要紧, 可以先阅读 [区块链上编程: DApp 开发简介] 进行前置知识补充.
随着加密猫, FOMO3D 等游戏的火爆, 去中心化应用在游戏领域遍地开花, 下面就带着大家一起开发一款简单有趣的 DApp 游戏, 帮助大家熟悉 DApp 开发. 本 DApp 实现的合约功能: 用户从 0-6 的数字中, 任意组合三位数进行投注, 合约计算出 3 位随机数, 根据随机数的组合规则分别给予用户不同倍数的奖励, 如随机数为 AAA ,A 等于 6 则奖励至少 20 倍投注金额, 即奖池所有余额; A 不等于 6 则奖励 5 倍投注金额; 随机数为 AAB, 则奖励 2 倍投注金额; 随机数为 ABC 则不奖励, 同时用户可查看奖池余额和个人投注记录.
合约编写
可以看出合约需要实现用户投注, 生成随机数, 发放奖励, 奖池余额查询的功能, 接下来编写我们的合约代码.
新建 Lottery.sol 合约文件, 声明合约版本,^ 表示合约编译版本为 0.4.0 至 0.5.0(不含 0.5.0).
pragma solidity ^0.4.0;
定义合约基本内容, 同时声明最低投注金额.
- contract Lottery {
- uint public betMoney = 10 finney;
- }
生成随机数, 通过区块难度 block.difficulty 和内置函数 keccak256 生成随机数, 在 EVM 中常用的数据存储位置: memory,storage, 函数的参数, 返回值默认存储在 memory 中, 状态变量默认存储在 storage 中, 我们可以通过声明 memory,storage 改变默认存储位置, 两者的存储都需要消耗 gas, 但 storage 的开销远大于 memory.
- contract Lottery {
- ...
- function generateRandomNumber() private view returns(uint[]) {
- uint[] memory dices = new uint[](3);
- for (uint i = 0; i <3; i++) {
- dices[i] = uint(keccak256(abi.encodePacked(block.difficulty, now, i))) % 6 + 1;
- }
- return dices;
- }
- ...
- }
获取合约余额, address 类型比较重要的成员属性主要有 balance,transfer, 分别为获取地址余额, 转移 eth 至该地址, 默认 eth 单位是 wei.
- contract Lottery {
- ...
- function getBankBalance() public view returns(uint) {
- return address(this).balance;
- }
- ...
- }
用户投注, 投注方法需要使用 payable 关键字描述, 用来表示可以接收 eth; 通过 msg.sender 和 msg.value 获得交易发送者地址和当前交易附带的 eth. 通常使用 require 来校验外部输入参数, 当判定条件为 false 时, 则会将剩余的 gas 退回, 同时回滚交易; assert 则用来处理合约内部的逻辑错误, 当错误发生时会消耗掉所有 gas, 同时回滚交易.
- contract Lottery {
- ...
- function bet() public payable {
- uint amount = msg.value;
- require(amount>= betMoney, 'bet money not Less than 10 finney');
- require(address(this).balance>= amount * 20, 'contract money not enough to pay reward');
- uint[] memory dices = generateRandomNumber();
- require(dices.length == 3, 'dices illegal');
- address addr = msg.sender;
- bool isReward = false;
- uint reward = 0;
- if ((dices[0] == dices[1]) && (dices[1] == dices[2]) && (dices[0] == 6)) {
- isReward = true;
- reward = address(this).balance;
- } else if ((dices[0] == dices[1]) && (dices[1] == dices[2]) && (dices[0] != 6)) {
- isReward = true;
- reward = amount * 5;
- } else if ((dices[0] == dices[1]) || (dices[0] == dices[2]) || (dices[1] == dices[2])) {
- isReward = true;
- reward = amount * 2;
- }
- if (isReward) {
- addr.transfer(reward);
- }
- }
- ...
定义事件, 通过合约内部触发事件, web3 监听到事件回调进行相应逻辑处理, 从而进行页面 UI 更新; 同时前端也可以通过对应事件名称获取日志信息.
- contract Lottery {
- ...
- event BetList(
- address addr,
- uint amount,
- bool isReward,
- uint reward,
- uint[] dices
- );
- function bet() public payable {
- ...
- emit BetList(addr, amount, isReward, reward, dices);
- }
- ...
与合约进行交互
至此, 我们已经写完了合约代码, 前端页面实现就不在此赘述, 主要介绍如何使用 web3 与合约交互, 这里使用到的 web3 版本是 1.0,web3 1.0 和 0.2x.x 的 API 调用方式差别较大, 1.0 的 API 支持异步调用.
安装 Metamask 浏览器插件后, 会在浏览器页面内注入一个 web3 实例. 检测页面中是否存在 web3 实例, 如果不存在则连接自己的实例.
- import Web3 from 'web3';
- if (typeof web3 !== 'undefined') {
- web3 = new Web3(web3.currentProvider);
- } else {
- web3 = new Web3(new Web3.providers.HttpProvider(NODE_NRL));
- }
传入合约 ABI, 合约地址, 实例化合约对象.
- this.contract = new web3.eth.Contract(
- CONTRACT_ABI,
- CONTRACT_ADDR,
- );
调用合约中的投注方法, 通过 try catch 可以捕获到 Metamask 弹窗取消交易操作.
- userBet = async () => {
- try {
- await this.contract.methods
- .bet(
- ...
- )
- .send({
- from: ACCOUNT,
- value: MONEY,
- });
- } catch (error) {
- ...
- }
- }
查询记录的日志, 可以通过指定事件名称, 区块高度及过滤条件来进行日志查询, 值得注意的是, 在合约内不能查询到日志信息.
- queryEvent = async () => {
- const event = await this.contract.getPastEvents(
- EVENT_NAME,
- {
- filter: {},
- fromBlock: 0,
- toBlock: 'latest',
- }
- )
- }
功能拓展
比如修改用户投注金额及充值这类敏感操作, 就需要管理员的权限来进行操作. 同样地, 我们也可以拓展赞助商的功能, 通过充值奖池的累计金额排名来展示赞助商的广告, 这里就不做展开了.
定义修饰器, 在构造函数里设置管理员地址, 将创建合约的账户设置为管理员.
- contract Lottery {
- ...
- address public owner;
- modifier onlyOwner() {
- require(owner == msg.sender);
- _;
- }
- constructor() public {
- owner = msg.sender;
- }
- ...
- }
实现修改投注金额的功能, 仅管理员账户可触发.
- contract Lottery {
- ...
- function setBetMoney(uint _betMoney) public onlyOwner {
- betMoney = _betMoney;
- }
- function deposit() public payable onlyOwner {}
- ...
- }
总结
当前随机数的实现通过链上信息生成, 这种生成随机数的方式容易受到不诚实的节点攻击. 下一篇文章将通过多个实例介绍相应的第三方工具库的使用 (Oricalize,SafeMath,OpenZepplin), 生成安全的随机数.
参考资料
- Solidity https://solidity.readthedocs.io/en/v0.4.24
- Web3js https://web3js.readthedocs.io/en/1.0
- Cryptozombies https://cryptozombies.io/zh/course
- Coursetro
前置文章:
区块链上编程: DApp 开发简介 https://mp.weixin.qq.com/s/dJuFG38MBn6m_NjpG_xa-A
(掘金社区链接点此)
文 / ielapp
一个简单的程序员
编 / 荧声
来源: https://juejin.im/post/5bbd924ef265da0ace215220