概述
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前后端开发
需要使用相关的api 及uosjs 相关库文件。
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企业版
开发工具:
生产环境
操作系统:ubuntu 16.04
工具:
智能合约
表结构(multi_index)
全局变量:
比大小押注记录:
轮盘投注记录:(以一击十、百里挑一通过scope来区分)
合约接口
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
Copy type OneshotInfo struct {
OsID uint64 `json:"osid"` //主键,押注id
Player string `json:"player"` //玩家
Amt int64 `json:"amt"` //金额
Celling int64 `json:"celling"` //押注点数
MicroSec int64 `json:"microsec,string"` //押注时间
}
未结算抽奖模式参与记录表格GroupTbl:主键为GroupItem.RltID、GrpType
Copy type GroupItem struct {
RltID int64 `json:"rltid"` //ID,主键
MicroSec int64 `json:"microsec,string"` //参与时间
Player string `json:"player"` //玩家
}
区块信息表格BlockInfoTbl:
比大小历史记录表格DiceHistoryTbl:
多人模式历史记录表格GroupHistoryTbl:
程序框架
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()
将所有需要调用的接口封装进该函数,创建多个线程并发调用这些函数。
Copy func (c * ResolverCtrl ) RunResolve () error {
c. initDB ()
if err := c. refreshHeadBlk (); nil != err {
log. Printf ( "main - ctrl.refreshHeadBlk failed : %v " , err)
return err
}
go c. fetchBlkInfoRoutine ()
go c. fetchOneshotsRoutine ()
go c. fetchGroupRoutine ( "group10" )
go c. fetchGroupRoutine ( "group100" )
go c. solveGroupRoutine ( "group10" )
go c. solveGroupRoutine ( "group100" )
c. solveOneshotsRoutine ()
return fmt. Errorf ( "Unwished exit from ResolverCtrl.Run" )
}
2. fetchBlkInfoRoutine()
获取区块信息,每隔10秒调用一次,仅查询并存储区块高度能整除10的区块的信息。该函数查询数据库表格BlockInfoTbl的最大索引,通过api命令get_block获取新的区块的信息并插入到表格BlockInfoTbl。
3. fetchOneshotsRoutine()
通过api命令get_table_rows获取比大小模式(未结算)押注记录,每隔1秒调用一次,因为UOS每隔1秒产生一个新的区块。该函数查询合约中比大小模式押注记录,如果合约中存在数据则更新本地数据库表OneshotTbl。
4. fetchGroupRoutine(group string)
参数:
通过api命令get_table_rows获取10人组和100人组(未结算)参与记录,每隔5秒调用一次,因为该模式需要10人或100参与才能开奖,时间间隔可以相对长一些。该函数查询合约中多人抽奖参与记录,如果合约中存在数据则更新本地数据库表GroupTbl。
5. solveGroupRoutine(group string)
参数:
结算多人组游戏模式,间隔10秒调用一次,该函数读取数据库表格GroupTbl中所有记录,如果达到结算要求(10人组不少于10人次,100人组不少于100人次)则进行抽奖结算。
6. solveOneshotsRoutine()
结算比大小游戏模式,间隔1秒调用一次,该函数读取数据库表格OneshotTbl中所有记录进行结算。
7. GetDiceHisTbl(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)
参数:
获取数据库表格GroupHistoryTbl中数据,前端调用该接口获取历史数据。实际相当于SQL语句:SELECT * FROM GroupHistoryTbl WHERE GrpType=group ORDER BY AccountActSeq DESC LIMIT limit OFFSET offset
前端
界面效果
基本流程
比大小 :用户登陆->选择押注金额及点数->点击“下注”按钮->构建交易->Usmart签名并发送交易->自动刷新页面
以一击十/百里挑一 :用户登录->点击“加入”按钮->构建交易->Usmart签名并发送交易->自动刷新页面
主要接口
1. GetGrpHisTbl() :查询GroupHistoryTbl表格,实际是对后端的GetGrpHisTbl接口进行一次封装。
dicegame/www/app/controllers/app.go
Copy func (c App ) GetGrpHisTbl (chain, group string , limit, offset int64 ) revel . Result {
/*与resolver程序交互,获取数据*/
grps, err := rsvCtrl. GetGrpHisTbl (group, limit, offset)
if nil != err {
return c. RenderJSON ( CommResp {
Result: - 1 ,
Desc: "DB query error" ,
Data: nil ,
})
}
return c. RenderJSON ( CommResp {
Result: 0 ,
Desc: "OK" ,
Data: grps,
})
}
2. GetBetHistory() :查询DiceHistoryTbl表格,实际是对后端的GetDiceHisTbl接口进行一次封装。
dicegame/www/app/controllers/app.go
Copy func (c App ) GetBetHistory (chain, player string , highBound uint64 , limit int ) revel . Result {
/*与resolver程序交互,获取数据*/
hises, err := rsvCtrl. GetDiceHisTbl (player, highBound, limit)
if nil != err {
log. Printf ( "App.GetBetHistory - failed : %v " )
return c. RenderJSON ( CommResp {
Result: - 1 ,
Desc: "DB query error" ,
Data: nil ,
})
}
return c. RenderJSON ( CommResp {
Result: 0 ,
Desc: "OK" ,
Data: hises,
})
}
3. login()登录:
dicegame/www/publilc/js/dice.js
Copy function login (){
if (cachedAccount != "" ){
console .log (cachedAccount + " has logged in" );
notify_message (cachedAccount + " has logged in" );
return ;
}
cachedAccount = getCookie ( "cachedAccount" );
if (cachedAccount == undefined || cachedAccount == "" ){
cachedAccount = "" ;
alert_message ( "请选择Usmart的登录帐号!" );
usmartAction ( 'uosio' , function (){ //身份验证并设置cookie
refresh_all (); //刷新余额
});
} else {
refresh_all ();
}
}
根据代码可以看出,该操作仅更新了cookie缓存(账户名),以及查询玩家账户的相关信息。该函数多个地方会调用:打开/刷新页面、点击“登录”按钮、点击“下注”/“加入”按钮。
4. logout()退出:
dicegame/www/publilc/js/dice.js
Copy function logout (){
cachedAccount = "" ;
setCookie ( "cachedAccount" , "" );
refresh_all ();
usmart .forgetIdentity () .then ((e) => {
console .log ( "logout ok" );
}) .catch (e => {
console .log ( "logout failed : " + e .message);
});
}
该操作清除cookie缓存,并刷新玩家的余额信息。
5. click_bet()下注:
dicegame/www/publilc/js/dice.js
Copy function click_bet (){
if ( "" == cachedAccount){
console .log ( "login" , "这里再登录一次" )
login ();
return ;
}
usmartAction ( get_transfer_contract () , function (contract , account){
var celling_slider = document .getElementById ( 'id_slider_celling' );
var bet_slider = document .getElementById ( 'id_slider_bet' );
var celling = parseInt ( celling_slider . noUiSlider .get ());
var tmp = bet_slider . noUiSlider .get ();
var bet = parseFloat (tmp) .toFixed ( 4 );
var invitor = getCookie ( "invitor" );
if (celling > MAX_CELLING || celling < MIN_CELLING ){
alert_message ( "押注标的应该在2到96之间!" );
console ( "押注标的应该在2到96之间,当前值:" + celling);
return ;
}
if ( parseFloat (bet) > MAX_BET || parseFloat (bet) < MIN_BET ){
alert_message ( "押注标的应该在0.5到10之间!" );
console ( "押注标的应该在0.5到10之间,当前值:" + bet);
return ;
}
const opts = { authorization : [ account .name + '@' + account .authority] };
var memostr = "oneshot;" + invitor + ";" + celling;
console .log ( "memostr" , memostr)
console.log("转账",`"from":${account.name},"to":${DICE_SERVANT}, "quantity":${(bet + " UOS")}, "memo":${memostr}`)
/* cluos push action uosio.token transfer '{"from":"player","to":"dice","quantity":"10.0000 UOS","memo":"oneshot;;50"}' -p player */
contract .transfer ( account .name , DICE_SERVANT , bet + " UOS" , memostr) .then (trx => {
console .log ( `Transaction ID: ${ trx .transaction_id } ` );
notify_message ( "操作成功!" )
}) .catch (e => {
alert_message ( "转账失败 :" + e .message);
console .error (e);
});
refresh_all (); //刷新页面(调用查询账户函数)
});
}
点击下注按钮,如果没有登录,则进行登录后返回(未下注);如果已经登录,则构建交易,并使用Usmart签名和发送交易。
6. click_grp()加入: 和click_bet类似
dicegame/www/publilc/js/dice.js
Copy function click_grp (grp){
if ( "" == cachedAccount){
login ();
return ;
}
if (grp != "group10" && grp != "group100" ){
console .log ( "grp param invalid : " + grp);
alert_message ( "grp param invalid : " + grp);
return ;
}
usmartAction ( get_transfer_contract () , function (contract , account){
var invitor = getCookie ( "invitor" );
const opts = { authorization : [ account .name + '@' + account .authority] };
var memostr = grp + ";" + invitor + ";" ;
contract .transfer ( account .name , DICE_SERVANT , "1.0000 UOS" , memostr) .then (trx => {
console .log ( `Transaction ID: ${ trx .transaction_id } ` );
notify_message ( "加入成功!" )
}) .catch (e => {
alert_message ( "转账失败 :" + e .message);
console .error (e);
});
refresh_all ();
});
}
7. updateBetHistory()刷新押注历史:
dicegame/www/publilc/js/dice.js
Copy function updateBetHistory (){
var high = 999999999 ;
var limit = 20 ;
var args = "highBound=" + high + "&limit=" + limit + "&blockchain=" + dice_blockchain;
$ .ajax ({type : 'post' ,
url : "/App/GetBetHistory?" + args ,
contentType : "application/json; charset=utf-8" ,
error :function (XMLHttpRequest , textStatus , errorThrown){
alert_message ( "查询押注历史失败!" + textStatus);
var msg = "" + textStatus + "." + errorThrown;
console .log ( "查询押注历史失败 : " + msg);
} ,
success :function (resp){
var tblobj = $ ( "#id_tbody_allhis" );
if ( resp .Result != 0 ){
console .log (resp)
var msg = "查询押注历史错误 : " + resp .Desc;
alert_message (msg);
console .log (msg);
return ;
}
tblobj .empty ();
for ( var idx = 0 ; idx < resp . Data . length ; idx ++ ){
var his = resp .Data[idx];
$ ( '<tr class="text-light">' +
'<td>' + his .Player + "@" + his .OsID + '</td>' +
'<td>' + his . Bet .replace ( " UOS" , "" ) + '</td>' +
'<td class=' + ((his.DiceVal < his.Celling) ? "text-danger lgfont" : "text-secondary") + '>' + his.Reward.replace(" UOS", "") + '</td>' +
'<td>' + his .DiceVal + "/" + his .Celling + '</td>' +
'<td class="text_left">' + ( new Date ( his .BetDate)) .Format ( "yyyy-MM-dd hh:mm:ss" ) + '</td>' +
'</tr>' ) .appendTo (tblobj);
}
} ,
});
}
该接口使用ajax获取并显示数据到页面,数据查询函数GetBetHistory。
8. 监听Usmart插件是否安装并加载:
dicegame/www/publilc/js/dice.js
Copy document .addEventListener ( 'usmartLoaded' , usmartExtension => {
usmart = window .usmart;
if (usmart == null ){
alert_message ( "请先安装并配置好Usmart!" );
return
}
if ( navigator . userAgent .includes ( "MdsApp" )){
window .usmart = null ;
} else {
delete window .usmart;
}
//自动登录
login ();
});
9. (主页)页面初始化:
dicegame/www/publilc/js/dice.js
Copy $ (document) .ready ( function (){ //初始化
…… //省略
var invitor = getURLParams ( "ref" )
if ( null != invitor){
setCookie ( "invitor" , invitor);
}
login (); //登录
setInterval (updateBetHistory , 3000 ); //设置定时器,刷新比大小记录
setInterval (refresh_group_wrapper , 3000 ); //刷新多人模式记录
$ ( "#id_welcome_modal" ) .modal ( 'show' );
});
初始化函数主要操作:获取推荐人并存储到cookie,玩家登录,刷新历史记录。