<Let's Move>SUI Move合约学习与实践——抢红包合约(sui-red-packet)

  • rzexin
  • 更新于 2024-03-31 18:37
  • 阅读 1221

SUI Move合约学习与实践——抢红包合约(sui-red-packet)

SUI Move合约学习与实践——抢红包合约(sui-red-packet)

1 合约说明

1.1 功能介绍

  • 本合约是一个基于SUI Move的抢红包合约
  • 任何人都可以创建红包,设置红包数量和红包总金额,红包可以是任意的代币类型
  • 可以指定可以领取红包的用户,如果不指定任何人都可以进行领取
  • 随机红包金额使用了weather-oracle大气压数据计算获得

1.2 合约代码

1.2.1 项目源码地址

https://github.com/movefuns/sui-red-packet

1.2.2 数据结构说明

(1)红包结构定义

成员变量说明:

  • sender:红包创建者
  • amount:红包个数
  • left_amount:红包剩余个数
  • coin_type:红包代币类型
  • coin_amount:红包代币余额
  • original_amount:初始红包余额
  • claimer_addresses:红包领取人地址列表
  • specified_recipient:定向红包指定接收人地址列表
  struct RedPacket&lt;phantom T> has key, store {
      id: UID,
      sender: address,
      amount: u64,
      left_amount: u64,
      coin_type: String,
      coin_amount: Balance&lt;T>,
      original_amount: u64,
      claimer_addresses: vector&lt;address>,
      specified_recipient: Option&lt;vector&lt;address>>
  }
(2)新建红包事件定义
  • red_packet_id:红包ID
  • sender:红包创建者地址
  • amount:红包个数
  • coin_type:代币类型
  • coin_amount:代币数量
  struct NewRedPacket&lt;phantom T> has copy, drop {
      red_packet_id: ID,
      sender: address,
      amount: u64,
      coin_type: String,
      coin_amount: u64,
  }
(3)领取红包事件定义
  • claim_red_packet_id:领取红包ID
  • claimer:红包领取者地址
  • claim_amount:领取红包数量
  • claim_coin_type:领取红包代币类型
  struct ClaimRedPacket&lt;phantom T> has copy, drop {
    claim_red_packet_id: ID,
    claimer: address,
    claim_amount: u64,
    claim_coin_type: String,
  }

1.2.3 接口说明

(1)创建红包(send_new_red_packet
  • 调用该接口会创建红包共享对象(RedPacket

  • 该红包共享对象会包含代币类型名称,获取方法是:

    let coin_type = type_name::get&lt;T>();
    let coin_type_string = *type_name::borrow_string(&coin_type);
  • 此外红包共享对象的成员变量还会包括红包创建者(sender)、红包个数(amount)、红包总金额(coin_amount)、红包指定接收人地址(specified_recipient

  • 抛出红包创建事件(NewRedPacket

  public entry fun send_new_red_packet&lt;T>(
      amount: u64,
      coin_amount: Coin&lt;T>,
      specified_recipient: Option&lt;vector&lt;address>>,
      ctx: &mut TxContext,
  ) {
    let sender = tx_context::sender(ctx);
    let id = object::new(ctx);
    let red_packet_id = object::uid_to_inner(&id);
    let coin_amount_num = coin::value(&coin_amount);

    let coin_amount = coin::into_balance(coin_amount);

    let coin_type = type_name::get&lt;T>();
    let coin_type_string = *type_name::borrow_string(&coin_type);

    event::emit(NewRedPacket&lt;T> {
        red_packet_id,
        sender,
        amount,
        coin_type: coin_type_string,
        coin_amount: coin_amount_num,
    });

    let red_packet = RedPacket&lt;T> {
        id,
        sender,
        amount,
        left_amount:amount,
        coin_type: coin_type_string,
        coin_amount,
        original_amount: coin_amount_num,
        claimer_addresses: vector::empty&lt;address>(),
        specified_recipient,
    };

    transfer::share_object(red_packet);
  }

注:因为命令行调用,无法传递空值,对做合约接口做了修改,去掉了可选入参specified_recipient

https://docs.sui.io/references/sui-api

修改后的合约代码如下:

  public entry fun send_new_red_packet&lt;T>(
      amount: u64,
      coin_amount: Coin&lt;T>,
      ctx: &mut TxContext,
  ) {
    let sender = tx_context::sender(ctx);
    let id = object::new(ctx);
    let red_packet_id = object::uid_to_inner(&id);
    let coin_amount_num = coin::value(&coin_amount);

    let coin_amount = coin::into_balance(coin_amount);

    let coin_type = type_name::get&lt;T>();
    let coin_type_string = *type_name::borrow_string(&coin_type);

    event::emit(NewRedPacket&lt;T> {
        red_packet_id,
        sender,
        amount,
        coin_type: coin_type_string,
        coin_amount: coin_amount_num,
    });

    let red_packet = RedPacket&lt;T> {
        id,
        sender,
        amount,
        left_amount:amount,
        coin_type: coin_type_string,
        coin_amount,
        original_amount: coin_amount_num,
        claimer_addresses: vector::empty&lt;address>(),
        specified_recipient: option::none(),
    };

    transfer::share_object(red_packet);
  }
(2)领取红包(claim_red_packet
  • 每个地址只能领取一次,重复领取将会报错:EAlreadyClaimed
  • 如果有设置红包指定接收人地址,那么只能是指定的接收地址可以领取,否则将会报错:ENotInSpecifiedRecipients
  • 若红包剩余数量只有一个,那么直接将红包余额全部转给接收者
  • 若红包剩余数量超过一个,则:
  • 将红包领取人地址添加到已领取红包列表中
  • 抛出红包领取事件(ClaimRedPacket
public entry fun claim_red_packet&lt;T>(red_packet:&mut RedPacket&lt;T>,weather_oracle: &WeatherOracle, clock: &Clock, ctx: &mut TxContext) {
    let sender = tx_context::sender(ctx);

    assert!(!vector::contains(&red_packet.claimer_addresses, &sender), EAlreadyClaimed);

    if(!option::is_none(&red_packet.specified_recipient)) {
      let specified = option::borrow(&red_packet.specified_recipient);
      assert!(vector::contains(specified, &sender), ENotInSpecifiedRecipients);
    };

    let left_value = balance::value(&red_packet.coin_amount);
    let coin_type = type_name::get&lt;T>();
    let coin_type_string = *type_name::borrow_string(&coin_type);

    let _log_claim_amount: u64 = 0;

    if (red_packet.left_amount == 1) {
      red_packet.left_amount = red_packet.left_amount - 1;
      let coin = coin::take(&mut red_packet.coin_amount, left_value, ctx);
      transfer::public_transfer(coin, sender);
      _log_claim_amount = left_value;
    } else {
      let max = (left_value / red_packet.left_amount) * 2;
      let claim_amount = get_random(weather_oracle,max,clock,ctx);
      let claim_split = balance::split(&mut red_packet.coin_amount, claim_amount);
      let claim_value = coin::from_balance(claim_split, ctx);
      red_packet.left_amount = red_packet.left_amount - 1;
      transfer::public_transfer(claim_value, sender);
      _log_claim_amount = claim_amount;
    };

    vector::push_back(&mut red_packet.claimer_addresses,sender);

    event::emit(ClaimRedPacket&lt;T> {
        claim_red_packet_id: object::uid_to_inner(&red_packet.id),
        claimer: sender,
        claim_amount: _log_claim_amount,
        claim_coin_type: coin_type_string,
    })
  }

1.2.4 关键逻辑说明

(1)获取天气数据随机数
  • 获取两个地点的大气压数据random_pressure_prandom_pressure_l(位置编号与城市名称的对应关系见:https://github.com/MystenLabs/apps/tree/main/weather-oracle#oracles
  • 将红包领取人地址、地点一的气压、地点二的气压、当前时间戳、交易哈希拼接成字节数组
  • 而后进行一次blake2b256编码,再将其经由bcs编码后转成u64整型
  • 再跟max进行取模运算后,获得随机数,该随机数机转账红包金额
  fun get_random(weather_oracle: &WeatherOracle, max: u64, clock: &Clock,ctx: &TxContext):u64{
    let sender = tx_context::sender(ctx);
    let tx_digest = tx_context::digest(ctx);
    let random_pressure_p = oracle::weather::city_weather_oracle_pressure(weather_oracle, 2988507);
    let random_pressure_l = oracle::weather::city_weather_oracle_pressure(weather_oracle, 88319);

    let random_vector = vector::empty&lt;u8>();
    vector::append(&mut random_vector, address::to_bytes(copy sender));
    vector::append(&mut random_vector, u32_to_ascii(random_pressure_p));
    vector::append(&mut random_vector, u32_to_ascii(random_pressure_l));
    vector::append(&mut random_vector, u64_to_ascii(clock::timestamp_ms(clock)));
    vector::append(&mut random_vector, *copy tx_digest);

    let temp1 = blake2b256(&random_vector);
    let random_num_ex = bcs::peel_u64(&mut bcs::new(temp1));
    let random_value = ((random_num_ex % max) as u64);
    debug::print(&random_value);
    random_value
  }

2 前置准备

2.1 帐号准备及角色分配

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

3 合约部署

切换到Jason账号

weather-oracle版本较老,需要使用较老的sui才能编译,还需要跳过依赖检查

export GAS_BUDGET=100000000
sui_1.19.1 client publish --gas-budget $GAS_BUDGET --skip-dependency-verification

image.png

部署合约虽然创建了AdminCapPublisher合约中并没有使用:

image.png

  • 记录PACKAGE_ID
export PACKAGE_ID=0xe5417558cf7edc87840fef347f294dc0fa7bdcd82c043e630c504d233c6b4784

4 合约交互

4.1 创建红包(send_new_red_packet

export GAS_BUDGET=10000000
export COUNT=3 # 红包数量3个

# 找Jason名下2个大MIST对象,一个用于支付gas,一个用于拆分出指定MIST数量的coin对象
sui client gas --json | jq '.[] | select(.mistBalance > 100000) | .gasCoinId' -r > output.txt
GAS=$(sed -n '1p' output.txt)
SPLIT_COIN=$(sed -n '2p' output.txt)

# 拆分出10000 MIST,存入红包中
export COIN=`sui client split-coin --coin-id $SPLIT_COIN --amounts 10000 --gas $GAS --gas-budget $GAS_BUDGET --json | jq -r '.objectChanges[] | select(.objectType=="0x2::coin::Coin&lt;0x2::sui::SUI>" and .type=="created") | .objectId'`

sui client call --function send_new_red_packet --package $PACKAGE_ID --module red_packet --type-args 0x2::sui::SUI --args $COUNT $COIN --gas-budget $GAS_BUDGET
  • 得到红包共享对象
export RED_POCKET=0xe4713158775a3ca05eb55099076be009f94e8f4fcdf354a34b108949c0f52ab7

image.png

  • 查看红包共享对象
sui client object $RED_POCKET

image.png

  • 抛出事件

image.png

4.2 领取红包(claim_red_packet

4.2.1 Alice领取红包

export WEATHER_ORACLE=0x1aedcca0b67b891c64ca113fce87f89835236b4c77294ba7e2db534ad49a58dc

sui client switch --address alice

sui client call --function claim_red_packet --package $PACKAGE_ID --module red_packet --type-args 0x2::sui::SUI --args $RED_POCKET $WEATHER_ORACLE 0x6 --gas-budget 10000000

image.png

  • 触发事件

image.png

  • 领取的红包对象

image.png

  • 当前红包共享对象

Alice领取后,剩余红包数量(left_amount)、红包余额(coin_amount)、领取人列表(claimer_addresses)都响应发生了变化。

sui client object $RED_POCKET

image.png

  • Alice再次领取,因领取地址重复,将会报错
Error executing transaction: Failure {
    error: "MoveAbort(MoveLocation { module: ModuleId { address: e5417558cf7edc87840fef347f294dc0fa7bdcd82c043e630c504d233c6b4784, name: Identifier(\"red_packet\") }, function: 2, instruction: 20, function_name: Some(\"claim_red_packet\") }, 1) in command 0",
}

4.2.2 Bob领取红包

sui client switch --address bob

sui client call --function claim_red_packet --package $PACKAGE_ID --module red_packet --type-args 0x2::sui::SUI --args $RED_POCKET $WEATHER_ORACLE 0x6 --gas-budget 10000000

image.png

  • 触发事件

image.png

  • 领取的红包对象

image.png

  • 当前红包共享对象

Bob领取后,剩余红包数量(left_amount)、红包余额(coin_amount)、领取人列表(claimer_addresses)都继续发生了变化。

sui client object $RED_POCKET

image.png

4.2.3 最后一个红包领取

切换到Jason,领取最后一个红包。

因为只剩最后一个红包了,最后一个用户将会一次性全部领取。

sui client switch --address jason

sui client call --function claim_red_packet --package $PACKAGE_ID --module red_packet --type-args 0x2::sui::SUI --args $RED_POCKET $WEATHER_ORACLE 0x6 --gas-budget 10000000

image.png

  • 触发事件

image.png

  • 领取的红包对象

image.png

  • 当前红包共享对象

最后一个红包被领取后,剩余红包数量(left_amount)归零、红包余额(coin_amount)归零、领取人列表(claimer_addresses)出现3个领取地址。

sui client object $RED_POCKET

image.png

5 前端交互

5.1 修改配置

将文件:src/App.tsx中的PACKAGE_ID改成我们自己刚才合约发布的PACKAGE_ID

image.png

const PACKAGE_ID =
  "0xe5417558cf7edc87840fef347f294dc0fa7bdcd82c043e630c504d233c6b4784";

5.2 修改代码

因我们将创建红包接口(send_new_red_packet)接口中的可选入参specified_recipient去掉了,方便命令行调试,前端代码中也需要相应将该字段去掉。

image.png

5.3 部署到vercel上

image.png

发布地址:https://github-movefuns-sui-red-packet.vercel.app/

5.4 创建红包(send_new_red_packet

image.png

image.png

创建红包后,得到新的抢红包连接:https://github-movefuns-sui-red-packet.vercel.app/?redpacket=0xa5b7871a39c7b03adfad3a123e8c47e38ade54d609eb47ec01bc404c1f384ff3

5.5 领取红包(claim_red_packet

  • 领取前

image.png

  • 领取中

image.png

  • 领取后

image.png

  • 领完

image.png

6 更多

如果要体验该红包Dapp请访问:https://sui-red-packet.vercel.app/

欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!

image.png

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

0 条评论

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