Move学习: 基础part3

盲盒NFT程序

本文将介绍如何使用Move语言编写一个盲盒NFT程序。该程序允许用户支付SUI资产,并随机生成具有不同稀有度的盲盒NFT。

1. 必要结构和对象

在实现盲盒NFT程序之前,我们需要定义以下核心数据结构:

  • 盲盒Collection:管理NFT的铸造规则、统计信息和资金。
  • NFT:代表生成的盲盒NFT对象。
  • 管理员权限:用于管理Collection的权限对象。
  • 事件:记录购买和铸造NFT的事件。

以下是这些结构的Move代码定义:

// === 数据结构 ===
public struct Collection has key {
    id: UID,
    name: String,
    price: u64,
    total_supply: u64,
    minted: u64,
    balance: Balance<SUI>,
}

public struct AdminCap has key, store {
    id:UID,
}

public struct NFT has key, store {
    id: UID,
    name: String,
    rarity: String,  // TODO enum Common/Rare/Legendary
    image_url: String,
}

// 事件
public struct BoxPurchased has copy, drop {
    buyer: address,
    nft_id: ID,
    rarity: String,
}

2. 管理员指令

管理员拥有特殊的权限来管理盲盒Collection。以下是两个主要的管理员函数:

2.1 创建Collection

create_collection 函数用于创建Collection对象,该对象作为Minting规则和统计的中央数据中心,并设置为共享状态以供所有用户访问。

// ==== admin functions
/// create collection
/// 创建了一个可供所有用户交互的“中央数据中心”,用来管理 Minting 规则、统计和资金
public fun create_collection(
    _: &AdminCap,
    name: String,
    price: u64,
    total_supply: u64,
    ctx: &mut TxContext
)  {
    let collection = Collection {
        id: object::new(ctx),
        name,
        price,
        total_supply,
        minted: 0,
        balance: balance::zero(),
    };
    transfer::share_object(collection); // 原本是owned 现在设为共享;任何人都可以访问
}

2.2 提取资金

withdraw 函数允许管理员从Collection中提取所有累积的资金到自己的钱包。

/// withdraw
public fun withdraw(
    _: &AdminCap,
    collection: &mut Collection,
    ctx: &mut TxContext
) {
    let amount = balance::value(&collection.balance);
    let coin = coin::take(&mut collection.balance, amount, ctx);
    transfer::public_transfer(coin, tx_context::sender(ctx));
}

3. 主函数entry

buy_and_open 是用户购买和打开盲盒NFT的主要入口函数。该函数执行以下步骤:

  1. 校验供应量:确保Collection中仍有可铸造的NFT。
  2. 校验支付金额:确认用户支付的SUI足够购买NFT。
  3. 处理支付:将用户支付的SUI存入Collection的余额。
  4. 生成随机稀有度:使用随机数生成器确定NFT的稀有度(Common: 60%, Rare: 30%, Legendary: 10%)。
  5. 铸造NFT:创建新的NFT对象。
  6. 发送事件:记录购买事件。
  7. 更新统计:增加已铸造NFT的数量。
  8. 转移NFT:将NFT发送给购买者。
/// purchase_and_open
public entry fun buy_and_open(
    collection: &mut Collection,
    r: &Random,
    payment: Coin<SUI>,
    ctx: &mut TxContext,
) {
    // check supply
    assert!(collection.total_supply > collection.minted,EBoxSoldOut);

    // check payment
    let paid = coin::value(&payment);
    assert!(paid >= collection.price, EInsufficientPayment);

    // pay
    balance::join(&mut collection.balance, coin::into_balance(payment));
    // collection
    let mut generator = random::new_generator(r, ctx);
    let rarity_roll = random::generate_u8_in_range(&mut generator, 1, 100);

    let (rarity, name) = if (rarity_roll <= 60) {
        (b"Common".to_string(), b"Common Mystery NFT".to_string())
    } else if (rarity_roll <= 90) {
        (b"Rare".to_string(), b"Rare Mystery NFT".to_string())
    } else {
        (b"Legendary".to_string(), b"Legendary Mystery NFT".to_string())
    };

    // mint NFT
    let nft = NFT {
        id:object::new(ctx),
        name,
        rarity,
        image_url: b"ipfs://placeholder".to_string(),
    };

    let nft_id = object::id(&nft);

    // 发送事件
    event::emit(BoxPurchased{
        buyer: tx_context::sender(ctx),
        nft_id,
        rarity: rarity,
    });

    // 更新计数
    collection.minted = collection.minted + 1;

    // 转移NFT给买家
    transfer::public_transfer(nft, tx_context::sender(ctx));
}

4. 涉及的Move操作和注意点

4.1 UID、对象与资产

在Move中,任何具有key能力的结构体,只要内部包含一个UID字段,就会被Sui视为一个链上对象。这些对象具有持久化和唯一性,是区块链资产的基础。

4.2 Transfer操作

sui::transfer::{}模块提供了针对不同所有权类型的存储操作:

  • Transfer:将对象发送到某个地址,使其进入地址所有(address owned)状态。
  • Freeze:将对象置为不可变(immutable),成为对外公开且永不改变的常量。
  • Share:将对象置为共享(shared)状态,任何人都可访问/修改。

4.3 Entry函数参数顺序

在Move的entry函数中,参数顺序有特定的约定:

  • “对象(Objects)” 总是排在 “简单数据(Primitives)” 前面;
  • “运行时提供的特殊对象”(如 &Random)总是排在 “交易上下文” (&mut TxContext) 的前面;
  • &mut TxContext 永远是最后一位。

4.4 NFT铸造行为

与Rust的区别

传统Rust语言

在传统Rust中,创建结构体实例只是在内存中创建数据结构,没有持久化或所有权转移机制。

Sui Move语言

Sui Move是以对象为中心的设计哲学:

  1. 对象ID和持久化:带有id: object::new(ctx)的结构体被视为链上对象,object::new(ctx)分配全局唯一ID。
  2. 所有权语义:创建对象后必须明确处理,否则编译失败。

在Move中,创建NFT对象的代码就是"铸造(Minting)"行为:

铸造定义:在区块链上首次创建具有唯一身份和持久化存储的新资产对象。

// mint NFT
let nft = NFT {
    id: object::new(ctx), // 赋予唯一的链上ID
    name,
    rarity,
    image_url: b"ipfs://placeholder".to_string(),
};

4.5 Object相关操作

  • object::new(ctx):在链上创建新对象(如AdminCap)。
  • object::id(&object):获取对象的唯一ID。

4.6 Balance相关操作

Balance是模块内部管理总资金的容器:

  • balance::zero():创建空的Balance容器,用于初始化。
  • balance::value(&balance):获取Balance中的余额。
  • balance::join(&mut target, source):将source合并到target中,消耗source。这是收款的标准第二步。
  • balance::withdraw(&mut balance, amount):从Balance中取出指定金额,返回新的Balance。

4.7 Coin相关操作

Coin是可交易的资产对象:

  • coin::value(&coin):获取Coin的面值。
  • coin::take(&mut balance, amount, ctx):从Balance中取出Coin,需要TxContext铸造新Coin对象。
  • coin::into_balance(coin):消耗Coin,返回其Balance。这是存入Collection余额的标准第一步。

其他Coin操作

  • coin::split:分割Coin(例如找零)。
  • coin::join:合并多个Coin。

4.8 Coin与Balance对比

操作 目标 输入需求 核心区别
coin::take 提现给用户 需要&mut TxContext铸造新Coin对象 返回Coin(可交易对象)
balance::withdraw 内部拆分价值 不需要&mut TxContext 返回Balance(不可交易容器)

简单来说

  • 想让用户拿到钱去花? → 使用coin::take
  • 想在内部账户间分钱? → 使用balance::withdraw