在ultrain的智能合约体系结构中,一个帐号要向另个帐号转帐UGAS,一般来说有两种方式可以选择,第一种是发起交易调用utrio.token的transfer方法,第二种是将帐号权限delegate给utrio.code,在合约内完成转帐。
  一般来说,在下注的时候使用第一种方式,在发奖的时候使用第二种方式来完成。

转帐的通知机制

在utrio.token的transfer方法中,默认会调用require_recepient(from); require_recepient(to)来通知from和to帐号(如果它们部署了合约的话)的transfer方法,并且将和转帐相关的参数传递过来。这样一来,在from和to帐号的合约中,就可以检测到转帐交易了。
  由于require_recepient方法产生的通知交易是以inline的形式被执行的,所以如果这些交易链条中有一笔失败了,那么就意味着整个交易都失败了。所以,也就达到了我们在合约中控制交易是否允许的目的。
  下面我们用一个具体的例子来演示一下怎样下注和实现在合约中开奖。

准备工作

1.准备帐号

在开始之前,我们需要准备两个帐号,house和player,并且给这两个帐号购买一些UGAS,以方便后续的测试。其中house帐号是庄家帐号,用来部署游戏合约;player帐号是玩家帐号,它参与到游戏中。

2.编写游戏合约

游戏合中,最需要注意的地方就是接受转帐信息并允许玩家加入游戏。但是这个地方可能产生巨大的漏洞,导致EOS中诸如“假EOS攻击”、“假充值通知”等等主要利用假转帐的攻击方式。这里是需要特别关注点。
  示例源码如下:

import "allocator/arena";
import { Contract } from "ultrain-ts-lib/src/contract";
import { Log } from "ultrain-ts-lib/src/log";
import { RNAME, NAME } from "ultrain-ts-lib/src/account";
import { Asset } from "ultrain-ts-lib/src/asset";
import { NEX } from "ultrain-ts-lib/lib/name_ex";

const Reward_None: u8 = 0;
const Reward_waiting: u8 = 1;
const Reward_done: u8 = 2;

class Gamer implements Serializable{
    account: account_name;
    reward_status: u8;

    primaryKey(): u64 { return this.account; }
}

@database(Gamer, "gamer")
class MasterContract extends Contract {
    db: DBManager<Gamer>;
    constructor(code: u64) {
        super(code);
        this.db = new DBManager<Gamer>(NAME("gamer"), this.receiver, NAME("gamer"));
    }

    private startGame(from: account_name): void {
        let joined = this.db.exists(from);
        ultrain_assert(!joined, RNAME(from) + " has joined this game yet.");
        Log.s("欢迎").s(RNAME(from)).s("加入游戏").flush();

        let gamer = new Gamer();
        gamer.account = from;
        gamer.reward_status = Reward_None;

        this.db.emplace(this.receiver, gamer);
    }

    // 接收utrio.token的transfer信息,无论自己是作为from还是to。
    @action
    transfer(from: account_name, to: account_name, val: Asset, memo: string): void {
        // from通过向this.receiver转帐的方式,申请加入了游戏
        if (from != this.receiver && to == this.receiver) {
            Log.s(RNAME(from)).s(" require to join this game.").flush();
            // ......
            // 其它的一些检查逻辑都没问题,开始游戏逻辑
            this.startGame(from);
        }
        // 从this.receiver帐号向其它人转帐
        else if ( from == this.receiver && to != this.receiver) {
            let player = new Gamer();
            let reward = this.db.get(to, player);
            // 如果不是向游戏获胜者转帐,将不被允许
            // 这意味着没有人可以从游戏帐号中转移资产
            ultrain_assert(reward && player.reward_status == Reward_waiting, RNAME(to) + " is not win the game. Why you get reward??");
            // 更新发奖状态
            player.reward_status = Reward_done;
            this.db.modify(this.receiver, player);

            Log.s(RNAME(to)).s(" get rewards. ").flush();
        }
        // 其它情况,一律不允许转帐,不接受转入,也不接受转出
        else {
            ultrain_assert(false, "Do not accept transfer operation for any other account!");
        }
    }

    // 开始发奖
    @action
    reveal(): void {
        let cursor = this.db.cursor();
        ultrain_assert(cursor.count > 0, "No player joined this game.");
        // 作为测试,随便挑一个发奖
        cursor.first();
        let gamer = cursor.get();

        // 设置中奖人的状态,在transfer中检查是否允许转帐。
        gamer.reward_status = Reward_waiting;
        this.db.modify(this.receiver, gamer);
        // 开始转帐给gamer.account,10.0000 UGAS
        Asset.transfer(this.receiver, gamer.account, new Asset(100000), "Congratulation! you win this game!");
    }

    // 过滤action接收原则,接收utrio.token的transfer事件, 如果不提供这个filter,将不能收到utrio.token的转帐通知
    public filterAction(originalReceiver: u64): boolean {
        // 本合约的transfer方法不接受直接调用,也不接受inline方式的调用。
        // 但是接受utrio.token的transfer方法
        return (originalReceiver == this.receiver && this.action != NEX("transfer")) || (originalReceiver == NAME("utrio.token") && this.action == NEX("transfer"));

        // 可以使用系统默认的filter代替上面的判断。
        // return Contract.filterAcceptTransferTokenAction(this.receiver, originalReceiver, this.action);
    }
}

上面的合约中,我们提供了详细的注解,而且也相当的简单,不再详述。

部署合约

编译成功之后,我们将合约部署到house帐号上。

测试运行

1.测试player申请加入游戏

部署成功之后,player可以通过转帐的方式加入到游戏中。

clultrain transfer player house "250.0000 UGAS" "player-request to join game" -p player

执行成功的话,会产生以下输出:

executed transaction: 8077d23ff48ea804d3c3555420ce90f6955b439fc36bb0f2d4caa401eef11b7a  168 bytes  1592 us
#   utrio.token <= utrio.token::transfer        {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"}
#        player <= utrio.token::transfer        {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"}
#         house <= utrio.token::transfer        {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"}
>> player require to join this game.欢迎player加入游戏
2.测试house向player发奖

已经有玩家加入了游戏,现在可以测试发奖流程了。由于发奖是由一个inline交易发起的(代码中的Assert.transfer()会发起一个inline交易),所以需要将hosuse的权限授权给utrio.token。

clultrain set account permission house active '{"threshold": 1,"keys": [{"key":"PUBLIC_KEY_OF_HOUSE","weight": 1}],"accounts": [{"permission":{"actor":"house","permission":"utrio.code"},"weight":1}]}' owner -p house

接下来,就可以发奖了:

clultrain push action house reveal '[]' -p house

发奖成功的话,会有类似下面的输出:

executed transaction: f11f4378c87a253ef2127ab42798355e72a05270916dfb62d0b4b38c866ffc46  104 bytes  1578 us
#         house <= house::reveal                ""
#   utrio.token <= utrio.token::transfer        {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"}
#         house <= utrio.token::transfer        {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"}
>> player get rewards.
#        player <= utrio.token::transfer        {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"}

这样我们就将奖励发给了玩家。