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 之旅!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!