SUI Move合约学习与实践——去中心化彩票(Sui Defi Lottery)

  • rzexin
  • 更新于 2024-03-17 17:05
  • 阅读 923

SUI Move合约学习与实践——去中心化彩票(Sui Defi Lottery)

SUI Move合约学习与实践——去中心化彩票(Sui Defi Lottery)

1 合约说明

1.1 功能介绍

本合约是一个简单的去中心化彩票游戏合约,允许任何玩家:

  • 创建彩票游戏(startLottery
  • 购买彩票(buyTicket
  • 结束彩票游戏,需到达结束时间(endLottery

彩票游戏结束后,玩家可以查看自己是否是彩票的幸运中奖者(checkIfWinner)。

获胜者获得所有奖金。

1.2 合约代码

1.2.1 合约源码地址

https://github.com/JoE11-y/Sui-Defi-Lottery/blob/main/sources/game.move

1.2.2 数据结构说明

(1)彩票对象

成员变量说明:

  • round: 抽奖游戏结束轮次(使用该轮次的dacade随机数签名可以结束抽奖游戏)
  • endTime: 购买彩票结束时间,超过该结束时间将无法购买彩票,格式:毫秒级别时间戳
  • noOfTickets: 已出售彩票数量
  • noOfPlayers: 购买彩票参与者数量
  • winner: 获胜者地址
  • winningTicket: 获胜者彩票编号
  • ticketPrice: 彩票售价
  • reward: 彩票奖池总奖金
  • status: 彩票状态,进行中or已结束
  • winnerClaimed: 中奖者是否已领取奖励
    struct Lottery has key {
        id: UID,
        round: u64,
        endTime: u64,
        noOfTickets: u64,
        noOfPlayers: u32,
        winner: Option<address>,
        winningTicket: Option<u64>,
        ticketPrice: u64,
        reward: Balance<SUI>,
        status: u64,
        winnerClaimed: bool,
    }
(2)参与者购买彩票记录对象

成员变量说明:

  • lotteryId:彩票游戏编号
  • tickets:彩票编号列表
    struct PlayerRecord has key, store {
        id: UID,
        lotteryId: ID,
        tickets: vector<u64>,
    }

1.2.3 接口说明

(1)开始彩票游戏(startLottery
  • 任何人都可以创建彩票游戏,创建的彩票游戏对象是一个共享对象
  • 在接口中指定彩票游戏结束轮次(round)、单张彩票售价(ticketPrice)、彩票游戏持续时间(lotteryDuration)、获取当前时间戳的clock对象
    public fun startLottery(round: u64, ticketPrice: u64, lotteryDuration: u64, clock: &Clock, ctx: &mut TxContext) {
        // lotteryDuration is passed in minutes,
        let endTime = lotteryDuration + clock::timestamp_ms(clock);

        // create Lottery
        let lottery = Lottery {
            id: object::new(ctx),
            round,
            endTime,
            noOfTickets: 0,
            noOfPlayers: 0,
            winner: option::none(),
            winningTicket: option::none(),
            ticketPrice,
            reward: balance::zero(),
            status: ACTIVE, 
            winnerClaimed: false,
        };

        // make lottery accessible by everyone
        transfer::share_object(lottery);
    }
(2)创建玩家记录对象(createPlayerRecord
  • 玩家记录对象与指定的彩票游戏相关
  • 将要记录该用户购买的彩票记录
    public fun createPlayerRecord(lottery: &mut Lottery, ctx: &mut TxContext) {
        // get lottery id
        let lotteryId = object::uid_to_inner(&lottery.id);

        // create player record for lottery ID
        let player = PlayerRecord {
            id: object::new(ctx),
            lotteryId,
            tickets: vector::empty(),
        };

        lottery.noOfPlayers = lottery.noOfPlayers + 1;

        transfer::public_transfer(player, tx_context::sender(ctx));
    }
(3)购买彩票(buyTicket
  • 任何人创建玩家记录对象后,便可以去购买彩票
  • 需要指定购买彩票的数量,并支付总的费用
  • 购买的彩票编号就是从当前彩票对象中取得最新的编号,以此递增到指定购买数量,购买的奖票编号会记录到玩家记录对象中
    // Anyone can buyticket after getting a playerRecord
    public fun buyTicket(lottery: &mut Lottery, playerRecord: &mut PlayerRecord, 
        noOfTickets: u64, amount: Coin<SUI>, clock: &Clock ) {

        // check if user is calling from right lottery
        assert!(object::id(lottery) == playerRecord.lotteryId, EWrongLottery);

        // check that lottery has not ended
        assert!(lottery.endTime > clock::timestamp_ms(clock), ELotteryEnded);

        // check that lottery state is stil 0
        assert!(lottery.status == ACTIVE, ELotteryEnded);

        // calculate the total amount to be paid
        let amountRequired = lottery.ticketPrice * noOfTickets;

        // check that coin supplied is equal to the total amount required
        assert!(coin::value(&amount) >= amountRequired, EPaymentTooLow);

        // add the amount to the lottery's balance
        let coin_balance = coin::into_balance(amount);
        balance::join(&mut lottery.reward, coin_balance);

        // increment no of tickets bought and update players ticket record
        let oldTicketsCount = lottery.noOfTickets;
        let newTicketId = oldTicketsCount;
        let newTotal = oldTicketsCount + noOfTickets;
        while (newTicketId < newTotal) {
            vector::push_back(&mut playerRecord.tickets, newTicketId);
            newTicketId = newTicketId + 1;
        };

        lottery.noOfTickets = lottery.noOfTickets + noOfTickets;
    }

上面接口缺少找零逻辑,进行优化:增加找零逻辑相关的代码:

// Anyone can buyticket after getting a playerRecord
    public fun buyTicket(lottery: &mut Lottery, playerRecord: &mut PlayerRecord, 
        noOfTickets: u64, amount: &mut Coin<SUI>, clock: &Clock, ctx: &mut TxContext ) {

        ......

        // check that coin supplied is equal to the total amount required
        assert!(coin::value(amount) >= amountRequired, EPaymentTooLow);

        let paid = coin::split(amount, amountRequired, ctx);

        // add the amount to the lottery's balance
        let coin_balance = coin::into_balance(paid);
        balance::join(&mut lottery.reward, coin_balance);

        ......
    }
(4)结束彩票游戏(endLottery
  • 当达到了结束时间,使用指定轮次的随机数签名,任何人可以终止彩票游戏
  • 彩票游戏结束后会产生中奖彩票编号
    // Anyone can end the lottery by providing the randomness of round.
    // randomness signature can be gotten from https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/<round>
    public fun endLottery(lottery: &mut Lottery, clock: &Clock, drand_sig: vector<u8>){
        // check that lottery has ended
        assert!(lottery.endTime < clock::timestamp_ms(clock), ELotteryNotEnded);

        // check that lottery state is stil 0
        assert!(lottery.status == ACTIVE, ELotteryEnded);

        verify_drand_signature(drand_sig, lottery.round);

        // The randomness is derived from drand_sig by passing it through sha2_256 to make it uniform.
        let digest = derive_randomness(drand_sig);

        lottery.winningTicket = option::some(safe_selection(lottery.noOfTickets, &digest));

        lottery.status = ENDED;
    }
(5)检查是否中奖(checkIfWinner
  • 在彩票游戏结束后,彩票持有人可以检查自己是否中奖
  • 判断是否中奖的原理,就是看自己的玩家记录对象彩票购买编号中是否包含了中奖的号码
   // Lottery Players can check if they won
    public fun checkIfWinner(lottery: &mut Lottery, player: PlayerRecord, ctx: &mut TxContext): bool {
        let PlayerRecord {id, lotteryId, tickets } = player;

        // check if user is calling from right lottery
        assert!(object::id(lottery) == lotteryId, EWrongLottery);

        // check that lottery state is ended
        assert!(lottery.status == ENDED, ELotteryNotEnded);

        // get winning ticket
        let winningTicket = option::borrow(&lottery.winningTicket);

        // check if winning ticket exists in lottery tickets
        let isWinner = vector::contains(&tickets, winningTicket);   

        if (isWinner){
            // check that winner has not claimed
            assert!(!lottery.winnerClaimed, ELotteryCompleted);
            // set user as winner
            lottery.winner = option::some(tx_context::sender(ctx));

            // get the reward
            let amount = balance::value(&lottery.reward);

            // wrap reward with coin
            let reward = coin::take(&mut lottery.reward, amount, ctx);

            transfer::public_transfer(reward, tx_context::sender(ctx));

            lottery.winnerClaimed = true ; 
        };

        // delete player record
        object::delete(id);

        isWinner
    }

2 前置准备

2.1 帐号准备及角色分配

别名 地址 角色
Jason 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a 彩票创建者
Alice 0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 买家1
Bob 0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 买家2
  • 将地址添加到环境变量
export JASON=0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
export ALICE=0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19
export BOB=0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0

3 合约部署

切换到Jason账号

sui client publish --gas-budget 100000000
  • 命令输出关键信息截图

image.png

  • 将关键的对象ID记录到环境变量,方便后续调用使用
export PACKAGE_ID=0x2540564cc1ce271bb2f5cca67ac775fc3def2701ba608ed0eb92dfed4ab15a0c

4 合约交互

4.1 开始彩票游戏(startLottery

# 获取drand随机源当前轮次
export BASE_ROUND=`curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest | jq .round`
echo $BASE_ROUND

# 结束轮次为30分钟后的轮次
export END_ROUND=$((BASE_ROUND + 20 * 30))

# 结束时间,30分钟对应的毫秒数
export LOTTERY_DURATION=$((60 * 30 * 1000))

# 单张彩票售价:100
export TICKET_PRICE=100

export GAS_BUDGET=100000000

sui client call --function startLottery --package $PACKAGE_ID --module lottery --args $END_ROUND $TICKET_PRICE $LOTTERY_DURATION 0x6 --gas-budget $GAS_BUDGET
  • 得到彩票共享对象
export LOTTERY=0xb59c586110f33afe8fd3737606e4afd4306c58aec494886ea41adb751ad078b4

image.png

  • 查看彩票共享对象
sui client object $LOTTERY

image.png

4.2 创建玩家记录对象(createPlayerRecord

  • 创建玩家Alice记录对象

切换到Alice

sui client call --function createPlayerRecord --package $PACKAGE_ID --module lottery --args $LOTTERY --gas-budget $GAS_BUDGET

image.png

export PLAYER_RECORE1=0x10a4b40c695223390daca729a47354acd7a5903a0e3d452c54046415c8384953
  • 创建玩家Bob记录对象

切换到Bob

sui client call --function createPlayerRecord --package $PACKAGE_ID --module lottery --args $LOTTERY --gas-budget $GAS_BUDGET

image.png

export PLAYER_RECORE2=0x355deffd7f8f7f2e00b346115bdc8a44e670f8a2d017c9af24d5b1a34e9e12ff

4.3 购买彩票(buyTicket

(1)Alice购买5张奖票

export AMOUNT=0xaa336e6e334debd8282b3f460f47c18da7bd69d78ded5c88acb33e749b583d28

export COUNT=5

sui client call --function buyTicket --package $PACKAGE_ID --module lottery --args $LOTTERY $PLAYER_RECORE1 $COUNT $AMOUNT 0x6 --gas-budget $GAS_BUDGET
  • 查看Alice玩家记录对象

image.png

  • 查看当前彩票共享对象

image.png

(2)Bob购买8张奖票

export AMOUNT=0x54060b76fa166806dfc99c5fcb569c9de0b6e40ded523e7ac6fe740c57d3ebbb

export COUNT=8

sui client call --function buyTicket --package $PACKAGE_ID --module lottery --args $LOTTERY $PLAYER_RECORE2 $COUNT $AMOUNT 0x6 --gas-budget $GAS_BUDGET
  • 查看Bob玩家记录对象

image.png

  • 查看当前彩票共享对象

image.png

(3)Alice再购买3张奖票

export AMOUNT=0xaa336e6e334debd8282b3f460f47c18da7bd69d78ded5c88acb33e749b583d28

export COUNT=3

sui client call --function buyTicket --package $PACKAGE_ID --module lottery --args $LOTTERY $PLAYER_RECORE1 $COUNT $AMOUNT 0x6 --gas-budget $GAS_BUDGET
  • 查看Alice玩家记录对象

image.png

  • 查看当前彩票共享对象

image.png

4.4 结束彩票游戏(endLottery

若游戏未结束调用将报错:

Error executing transaction: Failure {
    error: "MoveAbort(MoveLocation { module: ModuleId { address: 2540564cc1ce271bb2f5cca67ac775fc3def2701ba608ed0eb92dfed4ab15a0c, name: Identifier(\"lottery\") }, function: 3, instruction: 11, function_name: Some(\"endLottery\") }, 4) in command 0",
}
# 获取结束轮次的随机数签名
curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/$END_ROUND > output.txt
export SIGNATURE=0x`jq -r '.signature' output.txt`
echo $SIGNATURE

sui client call --function endLottery --package $PACKAGE_ID --module lottery --args $LOTTERY 0x6 $SIGNATURE --gas-budget $GAS_BUDGET
  • 查看当前彩票共享对象

得到获胜彩票编号为2,并标记彩票游戏已结束。

image.png

4.5 检查是否中奖(checkIfWinner

  • 玩家Alice兑奖
sui client call --function checkIfWinner --package $PACKAGE_ID --module lottery --args $LOTTERY $PLAYER_RECORE1 --gas-budget $GAS_BUDGET

Alice是中奖者,进行兑奖时,将会得到奖池中所有奖励:

image.png

image.png

  • 查看当前彩票共享对象

将会标记中奖者并将奖池清空

image.png

  • 玩家Bob兑奖

Bob未中奖,进行兑奖后,会将自己的玩家对象删除,查看对象时将报错:

sui client object $PLAYER_RECORE2

Internal error, cannot read the object: Object has been deleted object_id: 0x355deffd7f8f7f2e00b346115bdc8a44e670f8a2d017c9af24d5b1a34e9e12ff at version: SequenceNumber(26753603) in digest o#7gyGAp71YXQRoxmFBaHxofQXAipvgHyBKPyxmdSJxyvz

5 更多

欢迎关注微信公众号:Move中文,以及参与星航计划🚀,开启你的 Sui Move 之旅!

image.png

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
rzexin
rzexin
0x6Fa5...8165
江湖只有他的大名,没有他的介绍。