SUI Move合约学习与实践——去中心化彩票(Sui Defi Lottery)
本合约是一个简单的去中心化彩票游戏合约,允许任何玩家:
startLottery)buyTicket)endLottery)彩票游戏结束后,玩家可以查看自己是否是彩票的幸运中奖者(checkIfWinner)。
获胜者获得所有奖金。
https://github.com/JoE11-y/Sui-Defi-Lottery/blob/main/sources/game.move
成员变量说明:
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,
    }
成员变量说明:
lotteryId:彩票游戏编号tickets:彩票编号列表    struct PlayerRecord has key, store {
        id: UID,
        lotteryId: ID,
        tickets: vector<u64>,
    }
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);
    }
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));
    }
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);
        ......
    }
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;
    }
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
    }
| 别名 | 地址 | 角色 | 
|---|---|---|
| Jason | 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a | 
彩票创建者 | 
| Alice | 0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 | 
买家1 | 
| Bob | 0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0 | 
买家2 | 
export JASON=0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
export ALICE=0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19
export BOB=0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0
切换到Jason账号
sui client publish --gas-budget 100000000

export PACKAGE_ID=0x2540564cc1ce271bb2f5cca67ac775fc3def2701ba608ed0eb92dfed4ab15a0c
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

sui client object $LOTTERY

createPlayerRecord)切换到Alice
sui client call --function createPlayerRecord --package $PACKAGE_ID --module lottery --args $LOTTERY --gas-budget $GAS_BUDGET

export PLAYER_RECORE1=0x10a4b40c695223390daca729a47354acd7a5903a0e3d452c54046415c8384953
切换到Bob
sui client call --function createPlayerRecord --package $PACKAGE_ID --module lottery --args $LOTTERY --gas-budget $GAS_BUDGET

export PLAYER_RECORE2=0x355deffd7f8f7f2e00b346115bdc8a44e670f8a2d017c9af24d5b1a34e9e12ff
buyTicket)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


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


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


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,并标记彩票游戏已结束。

checkIfWinner)sui client call --function checkIfWinner --package $PACKAGE_ID --module lottery --args $LOTTERY $PLAYER_RECORE1 --gas-budget $GAS_BUDGET
Alice是中奖者,进行兑奖时,将会得到奖池中所有奖励:


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

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
欢迎关注微信公众号:Move中文,以及参与星航计划🚀,开启你的 Sui Move 之旅!

如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!