第十三章 DApp开发
概述
DApp是Decentralized Application的缩写,译为:分散式的应用程序。App我们都知道,我们在智能移动端上安装的应用程序也就是App。而DApp比App多了一个‘D’,‘D’的意思是分散式的。所以,它的意思是分散式的应用程序/去中心化的应用程序。
准备
为了能够开发UOS DApps,您需要懂得使用C / C ++技术开发,因为这是用于UOS智能合约的编程语言。在DApp开发之前,我们需要准备以下技能或工具:
1. C / C ++
2. Linux命令行知识
3. 编辑器或IDE(eclipse、clion、Sublime Text、vs code等)
4. 一个运行的本地测试网络节点
5. 前、后端服务器
基本流程
1. 创建账户
使用rpc命令cluos system newaccount。
2. 开发、编译合约
使用合约编译工具uosio.cdt进行编译
3. 部署、调试合约
使用rpc命令cluos set contract 部署合约。
由于没有专用的调试工具,一般使用print函数打印信息进行调试。
4. DApp前后端开发
5. 合约及前后端联调
6. 测试
注意事项
1. 通常情况下,DApp需要调用钱包,我们具有支持Chrome浏览器的钱包工具—Usmart。请到Chrome网上应用店查询Usmart并安装。
2. uosio.cdt并不能用于编译老版本的合约,因此开发合约时请按照uosio.cdt的语法规则及合约格式。
3. 合约调试时,注意设置配置文件config.ini中contracts-console选项为true。
4. uosjs还未完善,因此,DApp前后端开发会存在一些困难,后续我们将持续完善。
dicegame游戏
游戏说明
这是一个简单的区块链游戏,该游戏结合了骰子游戏和抽奖游戏,具有三种游戏模式:比大小、以一击十、百里挑一。
比大小:玩家在2-95中押注骰子点数,押注金额可选择0.5-10 UOS;当掷出的骰子点数(1-100)小于您所选的骰子点数,则您获胜,赢取金额为---投注金额 * 98/(押注点数 - 1)(精确到小数点后两位)。
以一击十:位玩家固定投注1 UOS(玩家可多次投注),满十人次投注即可开奖,从这十名玩家中随机选择一名玩家赢取奖金(9.5 UOS)。
百里挑一:每位玩家固定投注1 UOS(玩家可多次投注),满一百人投注即可开奖;从这一百名玩家中随机选择一名玩家,赢取奖金(95 UOS)。
架构
文件结构
该项目分分三个部分,contract(合约)、resolver(后台开奖程序,姑且称为后端)、www(采用revel框架,包含js、go、html等,姑且称为前端)。
合约及后端部分,文件相对较少,前端部分app文件夹存放go程序(该部分应该归属于后端部分,但这里不做明确区分)和html文件,conf文件夹存放revel的配置文件,public文件夹存放js、css、图片等。
系统架构
该Dapp主要包含四个部分:智能合约、前端、后端、数据库,该项目前、后端结合比较紧密,此处我们说的后端指狭隘的后端,仅指resolver(结算程序);前端指的是大前端,包含了一部分后端逻辑,我们也将Usmart归属于这部分。
简单的系统架构图如下:
根据上图,智能合约(contract)部署在区块链(blockchain)上,后端(resolver)与区块链进行交互,获取信息并存储在数据库(DB)中,如果存在需要结算的押注,则结算;前端读取数据库内容,更新到界面,如果有玩家参与游戏,则与Usmart交互,并将交易上链。
流程
1. 安装并配置Usmart钱包(配置网络、导入私钥、配置身份)。
2. 玩家在前端页面选择点数和押注金额,点击“下注”按钮,前端将会根据玩家的参数构建转账交易,Usmart签名并推送该交易上链。
3. 后端检索合约中表格,发现存在记录,则选择相应的区块ID对其进行开奖,并发起内联转账进行结算。
4. 后端将开奖记录入库,并展示到前端页面。
环境
开发环境
操作系统:Windows 10企业版
开发工具:
vs code
go
node js
c++
生产环境
操作系统:ubuntu 16.04
工具:
uos
uosio.cdt
go
node js
c++
sqlite3
revel框架
智能合约
表结构(multi_index)
全局变量:
字段名 | 字段类型 | 字段说明 |
id | uint64_t | 主键,ID |
next_osid | uint64_t | 比大小下一个ID |
next_grp10id | uint64_t | 以一击十下一个ID |
next_grp100id | uint64_t | 百里挑一下一个ID |
os_cnt | int64_t | 比大小当前参与人次 |
grp10_cnt | int64_t | 以一击十当前参与人次 |
grp100_cnt | int64_t | 百里挑一当前参与人次 |
freezed | bool | 冻结合约 |
min_celling | int8_t | 玩家选择点数下限 |
max_celling | int8_t | 玩家选择点数上限 |
min_amount | int64_t | 玩家押注金额下限 |
max_amount | int64_t | 玩家押注金额上限 |
比大小押注记录:
字段名 | 字段类型 | 字段说明 |
osid | uint64_t | 主键,ID |
player | name | 玩家 |
amt | uint64_t | 投入金额 |
celling | uint8_t | 投注点数 |
microsec | uint64_t | 投注时间(毫秒) |
轮盘投注记录:(以一击十、百里挑一通过scope来区分)
字段名 | 字段类型 | 字段说明 |
rltid | uint64_t | 主键,ID |
player | name | 玩家 |
microsec | uint64_t | 投注时间(毫秒) |
合约接口
1. onTransfer():
非action,该函数用于玩家进行押注,玩家通过向合约账户转账来触发该函数,合约通过解析备注来获取游戏模式,及其他参数,这并不是一个对外提供的接口,此处写明为了易于理解。
2. resolveos():
action,比大小模式结算,需要合约账户权限。
开奖结果计算公式:sum( sha256(未来区块ID + 游戏ID) ) % 100 + 1
3. resolvegrp():
action,以一击十、百里挑一模式开奖,需要合约账户权限。
开奖结果计算公式:sum ( sha256( 未来区块ID ) ) % 10 (或100)
后端(resolver)
运行原理
后端创建多个(7个)线程,4个线程用于获取链上数据,3个线程用于结算。每个线程都是一个死循环,数据获取线程获取到数据后会存入数据中;结算线程从数据库查询记录,如果有符合要求的数据则进行结算。数据获取线程:获取区块信息、获取比大小押注记录、获取以一击十参与记录、获取百里挑一参与记录;结算线程:结算比大小、结算以一击十、结算百里挑一。
表结构
未结算比大小押注记录表格OneshotTbl:主键为OneshotInfo.OsID
字段名 | 字段类型 | 字段说明 |
OneshotInfo | 自定义 | 投注信息(查询合约表格获取) |
Solved | bool | 是否结算 |
LastSolveSec | int64 | 结算时间 |
LastOnchainSec | int64 | 最后一次查找到该记录的时间戳 |
BlkNum | uint64 | 押注区块高度 |
BlkHash | string | 押注区块ID |
DiceVal | uint8 | 押注点数 |
未结算抽奖模式参与记录表格GroupTbl:主键为GroupItem.RltID、GrpType
字段名 | 字段类型 | 字段说明 |
GroupItem | 自定义 | 投注信息(查询合约表格获取) |
Solved | bool | 是否结算 |
LastSolveSec | int64 | 结算时间 |
LastOnchainSec | int64 | 最后一次查找到该记录的时间戳 |
GrpType | string | 主键,10人组或100人组 |
BlkNum | uint64 | 区块高度 |
BlkHash | string | 区块ID |
区块信息表格BlockInfoTbl:
字段名 | 字段类型 | 字段说明 |
BlockNum | uint64 | 主键,区块高度 |
MicroSec | int64 | 出块时间(毫秒) |
TmStr | string | 出块时间戳 |
BlockHash | string | 区块ID |
From | string | 区块节点rpcURL |
比大小历史记录表格DiceHistoryTbl:
字段名 | 字段类型 | 字段说明 |
ResolveDate | time.Time | 结算时间 |
OsID | uint64 | 主键,比大小押注ID |
BlkNum | uint64 | 用于结算的区块高度 |
BlkID | string | 用于结算的区块ID |
DiceVal | uint8 | 骰子点数 |
MicroSec | uint64 | 区块时间 |
Celling | uint8 | 上限 |
Player | string | 玩家 |
Result | string | 输赢结果 |
Bet | string | 押注金额 |
Reward | string | 返回金额 |
BetDate | string | 押注时间 |
AccountActSeq | uint64 | action序号 |
多人模式历史记录表格GroupHistoryTbl:
字段名 | 字段类型 | 字段说明 |
ResolveDate | string | 结算时间 |
BlkNum | uint64 | 区块高度 |
BlkID | string | 区块ID |
DiceVal | uint64 | 中奖序号(0-9或0-99 |
MicroSec | uint64 | 时间 |
Reward | string | 奖金 |
GrpType | string | 10人组或100人组 |
Winner | string | 中奖人 |
WinnerID | uint64 | 中奖人投注ID |
AccountActSeq | uint64 | 主键,action序号 |
程序框架
main()
└─RunResolve()
├─initDB()//初始化数据库
├─refreshHeadBlk()//获取投节点区块(即存储区块数据起始区块)
├─fetchBlkInfoRoutine()
│ ├─refreshHeadBlk()
│ └─fetchBlockInfo(blkNum uint64)
├─fetchOneshotsRoutine()
│ └─fetchOneshots()
├─fetchGroupRoutine("group10")
│ └─fetchGroup(group string)
├─fetchGroupRoutine("group100")
│ └─fetchGroup(group string)
├─solveGroupRoutine("group10")
│ └─doSolveGroup(group string, rangeBeginID int64)
│ └─execCmd(args []string)
├─solveGroupRoutine("group100")
│ └─doSolveGroup(group string, rangeBeginID int64)
│ └─execCmd(args []string)
└─solveOneshotsRoutine()
└─doSolveOneshot(os *OneshotInfo)
└─execCmd(args []string)
1. RunResolve()
将所有需要调用的接口封装进该函数,创建多个线程并发调用这些函数。
2. fetchBlkInfoRoutine()
获取区块信息,每隔10秒调用一次,仅查询并存储区块高度能整除10的区块的信息。该函数查询数据库表格BlockInfoTbl的最大索引,通过api命令get_block获取新的区块的信息并插入到表格BlockInfoTbl。
3. fetchOneshotsRoutine()
通过api命令get_table_rows获取比大小模式(未结算)押注记录,每隔1秒调用一次,因为UOS每隔1秒产生一个新的区块。该函数查询合约中比大小模式押注记录,如果合约中存在数据则更新本地数据库表OneshotTbl。
4. fetchGroupRoutine(group string)
参数:
名称 | 类型 | 说明 |
group | string | 游戏类型,group10/group100 |
通过api命令get_table_rows获取10人组和100人组(未结算)参与记录,每隔5秒调用一次,因为该模式需要10人或100参与才能开奖,时间间隔可以相对长一些。该函数查询合约中多人抽奖参与记录,如果合约中存在数据则更新本地数据库表GroupTbl。
5. solveGroupRoutine(group string)
参数:
名称 | 类型 | 说明 |
group | string | 游戏类型,group10/group100 |
结算多人组游戏模式,间隔10秒调用一次,该函数读取数据库表格GroupTbl中所有记录,如果达到结算要求(10人组不少于10人次,100人组不少于100人次)则进行抽奖结算。
6. solveOneshotsRoutine()
结算比大小游戏模式,间隔1秒调用一次,该函数读取数据库表格OneshotTbl中所有记录进行结算。
7. GetDiceHisTbl(player string, highBound uint64, limit int)
参数:
名称 | 类型 | 说明 |
player | string | 玩家账户,暂未使用 |
highBound | uint64 | 主键上限 |
limit | int | 返回条目限制 |
获取数据库表格DiceHistoryTbl中数据,前端调用该接口获取历史数据。实际相当于SQL语句:SELECT * FROM DiceHistoryTbl WHERE OsID< highBound ORDER BY OsID DESC LIMIT limit
8. GetGrpHisTbl(group string, limit, offset int64)
参数:
名称 | 类型 | 说明 |
group | string | 游戏类型 |
limit | int64 | 返回条目限制 |
offset | int64 | 偏移量 |
获取数据库表格GroupHistoryTbl中数据,前端调用该接口获取历史数据。实际相当于SQL语句:SELECT * FROM GroupHistoryTbl WHERE GrpType=group ORDER BY AccountActSeq DESC LIMIT limit OFFSET offset
前端
界面效果
基本流程
比大小:用户登陆->选择押注金额及点数->点击“下注”按钮->构建交易->Usmart签名并发送交易->自动刷新页面
以一击十/百里挑一:用户登录->点击“加入”按钮->构建交易->Usmart签名并发送交易->自动刷新页面
主要接口
1. GetGrpHisTbl():查询GroupHistoryTbl表格,实际是对后端的GetGrpHisTbl接口进行一次封装。
2. GetBetHistory():查询DiceHistoryTbl表格,实际是对后端的GetDiceHisTbl接口进行一次封装。
3. login()登录:
根据代码可以看出,该操作仅更新了cookie缓存(账户名),以及查询玩家账户的相关信息。该函数多个地方会调用:打开/刷新页面、点击“登录”按钮、点击“下注”/“加入”按钮。
4. logout()退出:
该操作清除cookie缓存,并刷新玩家的余额信息。
5. click_bet()下注:
点击下注按钮,如果没有登录,则进行登录后返回(未下注);如果已经登录,则构建交易,并使用Usmart签名和发送交易。
6. click_grp()加入:和click_bet类似
7. updateBetHistory()刷新押注历史:
该接口使用ajax获取并显示数据到页面,数据查询函数GetBetHistory。
8. 监听Usmart插件是否安装并加载:
9. (主页)页面初始化:
初始化函数主要操作:获取推荐人并存储到cookie,玩家登录,刷新历史记录。
Last updated