Sui by Example(中文版)

欢迎来到docs.sui.io的配套书籍。我们在Sui Move中介绍了Sui Move是Move语言的变体, 同时解释了如何使用 Sui Move 进行智能合约开发以及面向对象编程

English Version

Welcome to the companion book to docs.sui.io. There we describe the Sui Move variant of the Move programming language and explain how to use it to write smart contracts and programming with objects.

不同的是,在这本书中将基于智能合约示例来解释不同模块、概念的使用, 方便读者随时参考。毕竟在代码的世界中还有什么比代码本身更具表现力的呢!在这本书中你将看到 Sui Move 的大部分特性以及一些可以直接使用的高级设计模式以提升您的模块。

English Version

Instead, this site builds upon the smart contract examples already highlighted with component-by-component examples you may reference at any time. What is more expressive in the world of code than the code itself? In this book, you'll find examples for most of the features of Sui Move as well as a number of advanced patterns that can be used right away to improve your modules.

本书中所有示例都基于 Sui Move 开发, 您可以通过以下命令安装 Sui Move!!!请注意书中代码示例中所有的中文注释仅为翻译需要,实际开发中 move 语言暂不支持 UTF-8 编码注释。

$ cargo install --locked --git https://github.com/MystenLabs/sui.git --branch "main" sui

值得注意的是,上面的命令设置的分支为 main 主分支,如果在 devnet 网络开发请参考 install Sui

English Version

All code samples in this book are written with the assumption that you use Sui Move, which can installed with this command:

$ cargo install --locked --git https://github.com/MystenLabs/sui.git --branch "main" sui

Keep in mind that the branch is set to main. If you're developing with our devnet, instead follow the instructions to install Sui.

Sui Basics

在这个章节中将包括 Sui Move 的主要特性,请使用左侧导航栏浏览各个特性。

English Version

This section covers the main features of Sui Move. Use the left navigation menu to access the section's sub-pages.

Move.toml

每个 Move 包都包括一个清单文件 Move.toml--它位于包的根目录。清单本身包含了许多部分,其中最主要的是:

  • [package] - 包括包相关的元数据,例如名字, 作者等
  • [dependencies] - 声明项目的依赖
  • [addresses] - 地址别名(例如 @me0x0 的别名)
English Version

Every Move package has a package manifest in the form of a Move.toml file - it is placed in the root of the package. The manifest itself contains a number of sections, primary of which are:

  • [package] - includes package metadata such as name and author
  • [dependencies] - specifies dependencies of the project
  • [addresses] - address aliases (eg @me will be treated as a 0x0 address)
[package] 
name = "sui-by-example"
version = "0.1.0"

[dependencies] 
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "main" }

[addresses]  
examples = "0x0"

初始化函数(Init Function)

初始化函数(init)是一个只在模块发布时被执行一次的函数,函数签名总是一致和且只有一个参数:

fun init(ctx: &mut TxContext) { /* ... */ }
English Version

Init function is a special function that gets executed only once - when the associated module is published. It always has the same signature and only one argument:

fun init(ctx: &mut TxContext) { /* ... */ }

示例:

module examples::one_timer {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    /// `CreatorCapability`对象将只在模块发布时创建一次
    /// The one of a kind - created in the module initializer.
    struct CreatorCapability has key {
        id: UID
    }

    /// `init`函数只会执行一次,在这个例子中只有模块的发布者拥有`CreatorCapability`
    /// This function is only called once on module publish.
    /// Use it to make sure something has happened only once, like
    /// here - only module author will own a version of a
    /// `CreatorCapability` struct.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(CreatorCapability {
            id: object::new(ctx),
        }, tx_context::sender(ctx))
    }
}

入口函数(Entry Functions)

入口函数(entry function)修饰符用于可以直接被调用的函数,例如在交易(transaction)中直接调用。它可以和其它的函数可见性修饰符配合使用, 例如 public, 这样可以使函数被其他模块(module)调用, 或者配合 public(entry) 使用, 使得函数可以被 friend 模块调用。

English Version

An entry function visibility modifier allows a function to be called directly (eg in transaction). It is combinable with other visibility modifiers, such as public which allows calling from other modules) and public(friend) for calling from friend modules.

module examples::object {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    struct Object has key {
        id: UID
    }

    /// 被`public`修饰的的函数可以被任意模块调用
    /// 被`entry`修饰的函数不可以返回值
    /// If function is defined as public - any module can call it.
    /// Non-entry functions are also allowed to have return values.
    public fun create(ctx: &mut TxContext): Object {
        Object { id: object::new(ctx) }
    }

    /// 入口函数不可以拥有返回值因为它们可以在交易中被直接调用,返回值也不可用
    /// 如果入口`entry`函数如果没有同时被`public`修饰将不可以被其他模块调用
    /// Entrypoints can't have return values as they can only be called
    /// directly in a transaction and the returned value can't be used.
    /// However, `entry` without `public` disallows calling this method from
    /// other Move modules.
    entry fun create_and_transfer(to: address, ctx: &mut TxContext) {
        transfer::transfer(create(ctx), to)
    }
}

字符串(Strings)

Move 没有原生的字符串类型但是提供了方便的包装方法。

English Version

Move does not have a native type for strings, but it has a handy wrapper!

module examples::strings {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    // 使用以下依赖可以使用针对UTF-8的包装函数
    // Use this dependency to get a type wrapper for UTF-8 strings
    use std::string::{Self, String};

    /// 一个包含字符串类型的模拟对象
    /// A dummy Object that holds a String type
    struct Name has key, store {
        id: UID,

        /// Here it is - the String type
        name: String
    }

    /// 用输入的字节数据创建一个`Name`对象
    /// Create a name Object by passing raw bytes
    public fun issue_name_nft(
        name_bytes: vector<u8>, ctx: &mut TxContext
    ): Name {
        Name {
            id: object::new(ctx),
            name: string::utf8(name_bytes)
        }
    }
}

共享对象(Shared Object)

共享对象(Share object)可以通过 sui::transfer::share_object 被共享,从而让所有人都可以使用。

English Version

Shared object is an object that is shared using a sui::transfer::share_object function and is accessible to everyone.

/// 与`Owned`对象不同,`Shared`对象可以被任何人使用, 因此需要根据需求在逻辑中设计额外的安全检查
/// Unlike `Owned` objects, `Shared` ones can be accessed by anyone on the
/// network. Extended functionality and accessibility of this kind of objects
/// requires additional effort by securing access if needed.
module examples::donuts {
    use sui::transfer;
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::object::{Self, UID};
    use sui::balance::{Self, Balance};
    use sui::tx_context::{Self, TxContext};

    /// 错误码 0:对应所付费用低于甜甜圈价格
    /// For when Coin balance is too low.
    const ENotEnough: u64 = 0;

    /// 商店所有者权限凭证:获取利润
    /// Capability that grants an owner the right to collect profits.
    struct ShopOwnerCap has key { id: UID }

    /// 一个可被购买的甜甜圈对象
    /// A purchasable Donut. For simplicity's sake we ignore implementation.
    struct Donut has key { id: UID }

    /// 一个共享对象(需要具备`key`)
    /// A shared object. `key` ability is required.
    struct DonutShop has key {
        id: UID,
        price: u64,
        balance: Balance<SUI>
    }

    /// 通常可以在在初始化函数中创建共享对象,因为初始化函数只被执行一次
    /// Init function is often ideal place for initializing
    /// a shared object as it is called only once.
    /// 使用`transfer::share_object`共享对象
    /// To share an object `transfer::share_object` is used.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(ShopOwnerCap {
            id: object::new(ctx)
        }, tx_context::sender(ctx));

        // 通过共享对象使其可以被任何人使用
        // Share the object to make it accessible to everyone!
        transfer::share_object(DonutShop {
            id: object::new(ctx),
            price: 1000,
            balance: balance::zero()
        })
    }

    /// 任何拥有`Coin`的用户都可以调用`buy_donut`入口函数
    /// Entry function available to everyone who owns a Coin.
    public entry fun buy_donut(
        shop: &mut DonutShop, payment: &mut Coin<SUI>, ctx: &mut TxContext
    ) {
        assert!(coin::value(payment) >= shop.price, ENotEnough);

        // 从Coin<SUI>分离 amount = `shop.price`的对象
        // Take amount = `shop.price` from Coin<SUI>
        let coin_balance = coin::balance_mut(payment);
        let paid = balance::split(coin_balance, shop.price);

        // 存入商店的收支中
        // Put the coin to the Shop's balance
        balance::join(&mut shop.balance, paid);

        transfer::transfer(Donut {
            id: object::new(ctx)
        }, tx_context::sender(ctx))
    }

    /// 吃掉甜甜圈 :)
    /// Consume donut and get nothing...
    public entry fun eat_donut(d: Donut) {
        let Donut { id } = d;
        object::delete(id);
    }

    /// 收集利润,需要`ShopOwnerCap`凭证
    /// Take coin from `DonutShop` and transfer it to tx sender.
    /// Requires authorization with `ShopOwnerCap`.
    public entry fun collect_profits(
        _: &ShopOwnerCap, shop: &mut DonutShop, ctx: &mut TxContext
    ) {
        let amount = balance::value(&shop.balance);
        let profits = coin::take(&mut shop.balance, amount, ctx);

        transfer::public_transfer(profits, tx_context::sender(ctx))
    }
}

转移(Transfer)

同时拥有 keystore 的对象才可以被随意转移。

English Version

To make an object freely transferable, use a combination of key and store abilities.

/// 一个用于任意转移的封装模块
/// A freely transfererrable Wrapper for custom data.
module examples::wrapper {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    /// 当一个对象具有`store`属性时可以在任何模块中进行转移,同时
    /// 不需要自定义的转移函数
    /// An object with `store` can be transferred in any
    /// module without a custom transfer implementation.
    struct Wrapper<T: store> has key, store {
        id: UID,
        contents: T
    }

    /// 视图函数,用于获取`content`的内容
    /// View function to read contents of a `Container`.
    public fun contents<T: store>(c: &Wrapper<T>): &T {
        &c.contents
    }

    /// 任何人都可以创建一个新的对象
    /// Anyone can create a new object
    public fun create<T: store>(
        contents: T, ctx: &mut TxContext
    ): Wrapper<T> {
        Wrapper {
            contents,
            id: object::new(ctx),
        }
    }

    /// 摧毁封装对象,获取其封装的`contents`
    /// Destroy `Wrapper` and get T.
    public fun destroy<T: store> (c: Wrapper<T>): T {
        let Wrapper { id, contents } = c;
        object::delete(id);
        contents
    }
}

module examples::profile {
    use sui::transfer;
    use sui::url::{Self, Url};
    use std::string::{Self, String};
    use sui::tx_context::{Self, TxContext};

    // 声明对封装模块的引用
    // using Wrapper functionality
    use 0x0::wrapper;

    /// 档案信息不是一个对象,可以被包括在可被转移的容器中
    /// Profile information, not an object, can be wrapped
    /// into a transferable container
    struct ProfileInfo has store {
        name: String,
        url: Url
    }

    /// 读取`ProfileInfo`中的`name`项
    /// Read `name` field from `ProfileInfo`.
    public fun name(info: &ProfileInfo): &String {
        &info.name
    }

    /// 读取`ProfileInfo`中的`url`项
    /// Read `url` field from `ProfileInfo`.
    public fun url(info: &ProfileInfo): &Url {
        &info.url
    }

    /// 创建新的`ProfileInfo`同时封装在`Wrapper`对象中
    /// 将创建的对象转移给交易的发起者
    /// Creates new `ProfileInfo` and wraps into `Wrapper`.
    /// Then transfers to sender.
    public fun create_profile(
        name: vector<u8>, url: vector<u8>, ctx: &mut TxContext
    ) {
        // create a new container and wrap ProfileInfo into it
        let container = wrapper::create(ProfileInfo {
            name: string::utf8(name),
            url: url::new_unsafe_from_bytes(url)
        }, ctx);

        // `Wrapper` type is freely transferable
        transfer::public_transfer(container, tx_context::sender(ctx))
    }
}

自定义转移(Custom transfer)

在 Sui Move 中,只拥有 key 能力的对象的所有权是不可以被随意转移的, 想要使只有 key 限制符对象可以变更所有权,必须创建一个自定义的转移函数(transfer)。 这个函数可以包括任意的参数, 例如在转移对象时所需的费用等(如示例中)。

English Version

In Sui Move, objects defined with only key ability can not be transferred by default. To enable transfers, publisher has to create a custom transfer function. This function can include any arguments, for example a fee, that users have to pay to transfer.

module examples::restricted_transfer {
    use sui::tx_context::{Self, TxContext};
    use sui::balance::{Self, Balance};
    use sui::coin::{Self, Coin};
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::sui::SUI;
    
    /// 错误码 0: 对应所付费用与转移费用不符
    /// For when paid amount is not equal to the transfer price.
    const EWrongAmount: u64 = 0;

    /// 可以产生新`TitleDeed`的权限凭证
    /// A Capability that allows bearer to create new `TitleDeed`s.
    struct GovernmentCapability has key { id: UID }

    /// 用于表示产权的凭证,只能有具备`GovernmentCapability`权限的账户产生
    /// An object that marks a property ownership. Can only be issued
    /// by an authority.
    struct TitleDeed has key {
        id: UID,
        // ... some additional fields
    }

    /// 一个中心化的财产所有权变更和收费的登记处
    /// A centralized registry that approves property ownership
    /// transfers and collects fees.
    struct LandRegistry has key {
        id: UID,
        balance: Balance<SUI>,
        fee: u64
    }

    /// 在模块初始化时创建`GovernmentCapability`和`LandRegistry`对象
    /// Create a `LandRegistry` on module init.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(GovernmentCapability {
            id: object::new(ctx)
        }, tx_context::sender(ctx));

        transfer::share_object(LandRegistry {
            id: object::new(ctx),
            balance: balance::zero<SUI>(),
            fee: 10000
        })
    }

    /// 为产权所有人(`for`)创建`TitleDeed`, 只有`GovernmentCapability`的
    /// 所有者才可以创建
    /// Create `TitleDeed` and transfer it to the property owner.
    /// Only owner of the `GovernmentCapability` can perform this action.
    public entry fun issue_title_deed(
        _: &GovernmentCapability,
        for: address,
        ctx: &mut TxContext
    ) {
        transfer::transfer(TitleDeed {
            id: object::new(ctx)
        }, for)
    }

    /// 因为`TitleDeed`没有`store` ability 所以需要一个自定义的转移函数来变更所有权。
    /// 在这个例子中所有变更`TitleDeed`所有权的操作都需要支付`registry.fee`。
    /// A custom transfer function. Required due to `TitleDeed` not having
    /// a `store` ability. All transfers of `TitleDeed`s have to go through
    /// this function and pay a fee to the `LandRegistry`.
    public entry fun transfer_ownership(
        registry: &mut LandRegistry,
        paper: TitleDeed,
        fee: Coin<SUI>,
        to: address,
    ) {
        assert!(coin::value(&fee) == registry.fee, EWrongAmount);

        // 将支付的费用转给`LandRegistry`
        // add a payment to the LandRegistry balance
        balance::join(&mut registry.balance, coin::into_balance(fee));

        // 调用转移函数`transfer` 完成所有权变更
        // finally call the transfer function
        transfer::transfer(paper, to)
    }
}

事件(Events)

事件(event)是用来追踪链上行为的主要方式。

English Version

Events are the main way to track actions on chain.

/// 在共享对象示例的基础上加入事件(Event)
/// Extended example of a shared object. Now with addition of events!
module examples::donuts_with_events {
    use sui::transfer;
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::object::{Self, ID, UID};
    use sui::balance::{Self, Balance};
    use sui::tx_context::{Self, TxContext};

    // 使用事件是只需要添加`sui::event`依赖
    // This is the only dependency you need for events.
    use sui::event;

    /// 错误码 0:对应所付费用低于甜甜圈价格   
    /// For when Coin balance is too low.
    const ENotEnough: u64 = 0;

    /// 商店所有者权限凭证:获取利润
    /// Capability that grants an owner the right to collect profits.
    struct ShopOwnerCap has key { id: UID }

    /// 一个可被购买的甜甜圈对象
    /// A purchasable Donut. For simplicity's sake we ignore implementation.
    struct Donut has key { id: UID }

    struct DonutShop has key {
        id: UID,
        price: u64,
        balance: Balance<SUI>
    }

    // ====== Events ======

    /// 当用户购买甜甜圈时触发本事件
    /// For when someone has purchased a donut.
    struct DonutBought has copy, drop {
        id: ID
    }

    /// 当店主提取利润时触发本事件
    /// For when DonutShop owner has collected profits.
    struct ProfitsCollected has copy, drop {
        amount: u64
    }

    // ====== Functions ======

    fun init(ctx: &mut TxContext) {
        transfer::transfer(ShopOwnerCap {
            id: object::new(ctx)
        }, tx_context::sender(ctx));

        transfer::share_object(DonutShop {
            id: object::new(ctx),
            price: 1000,
            balance: balance::zero()
        })
    }

    /// 购买一个甜甜圈
    /// Buy a donut.
    public entry fun buy_donut(
        shop: &mut DonutShop, payment: &mut Coin<SUI>, ctx: &mut TxContext
    ) {
        assert!(coin::value(payment) >= shop.price, ENotEnough);

        let coin_balance = coin::balance_mut(payment);
        let paid = balance::split(coin_balance, shop.price);
        let id = object::new(ctx);

        balance::join(&mut shop.balance, paid);

        // 生成包含新甜甜圈ID的`DonutBought`事件
        // Emit the event using future object's ID.
        event::emit(DonutBought { id: object::uid_to_inner(&id) });
        transfer::transfer(Donut { id }, tx_context::sender(ctx))
    }

    /// 吃掉甜甜圈 :)
    /// Consume donut and get nothing...
    public entry fun eat_donut(d: Donut) {
        let Donut { id } = d;
        object::delete(id);
    }

    /// 收集利润,需要`ShopOwnerCap`凭证
    /// Take coin from `DonutShop` and transfer it to tx sender.
    /// Requires authorization with `ShopOwnerCap`.
    public entry fun collect_profits(
        _: &ShopOwnerCap, shop: &mut DonutShop, ctx: &mut TxContext
    ) {
        let amount = balance::value(&shop.balance);
        let profits = coin::take(&mut shop.balance, amount, ctx);

        // 生成包含转账金额的`ProfitsCollected`事件
        // simply create new type instance and emit it
        event::emit(ProfitsCollected { amount });

        transfer::public_transfer(profits, tx_context::sender(ctx))
    }
}

一次性见证(One Time Witness)

一次性见证(One Time Witness, OTW)是一种特殊类型的实例,仅在模块初始化器中创建,并保证是唯一的,并且只有一个实例。在需要确保仅执行一次见证授权操作的情况下非常重要(例如-创建新的Coin)。在 Sui Move中,如果一个类型的定义具有以下属性,则被视为 OTW:

  • 以模块的名字命名,但是所有字母大写
  • 只拥有 drop 修饰符

可以使用sui::types::is_one_time_witness(witness)来检查一个实例是不是OTW。

English Version

One Time Witness (OTW) is a special instance of a type which is created only in the module initializer and is guaranteed to be unique and have only one instance. It is important for cases where we need to make sure that a witness-authorized action was performed only once (for example - creating a new Coin). In Sui Move a type is considered an OTW if its definition has the following properties:

  • Named after the module but uppercased
  • Has only drop ability

To check whether an instance is an OTW, sui::types::is_one_time_witness(witness) should be used.

为了得到这个类型的实例,我们需要把它作为第一个参数传入到 visibilityinit() 函数: Sui 运行时自动提供两个初始化参数。

module examples::mycoin {

    /// 以模块的名字命名
    /// Name matches the module name
    struct MYCOIN has drop {}

    /// 将OTW作为第一个参数传入`init` 函数
    /// The instance is received as the first argument
    fun init(witness: MYCOIN, ctx: &mut TxContext) {
        /* ... */
    }
}
English Version

To get an instance of this type, you need to add it as the first argument to the init() function: Sui runtime supplies both initializer arguments automatically.

module examples::mycoin {

    /// Name matches the module name
    struct MYCOIN has drop {}

    /// The instance is received as the first argument
    fun init(witness: MYCOIN, ctx: &mut TxContext) {
        /* ... */
    }
}

通过以下例子我们可以更好地了解如何使用OTW:

/// 在这个例子中我们将解释OTW如何工作
/// This example illustrates how One Time Witness works.
/// 一次性见证(OTW)是一个在整个系统中唯一的实例。它具有以下属性:
/// One Time Witness (OTW) is an instance of a type which is guaranteed to
/// be unique across the system. It has the following properties:
///
/// - created only in module initializer | 只可以在`init`函数中创建
/// - named after the module (uppercased) | 使用模块名命名(大写)
/// - cannot be packed manually | 无法手动构造
/// - has a `drop` ability | 拥有`drop`属性
module examples::one_time_witness_registry {
    use sui::tx_context::TxContext;
    use sui::object::{Self, UID};
    use std::string::String;
    use sui::transfer;

    // 通过使用`sui::types`检查对象类型是否为OTW
    // This dependency allows us to check whether type
    // is a one-time witness (OTW)
    use sui::types;

    /// 错误码0: 当传入对象非OTW时触发
    /// For when someone tries to send a non OTW struct
    const ENotOneTimeWitness: u64 = 0;

    /// 此类型的对象将标记存在一种类型,每种类型只能有一条记录。
    /// An object of this type will mark that there's a type,
    /// and there can be only one record per type.
    struct UniqueTypeRecord<phantom T> has key {
        id: UID,
        name: String
    }

    /// 提供一个公共函数用于注册`UniqueTypeRecord`对象
    /// `is_one_time_witness`将确保每个泛型(T)只可使用这个函数一次
    /// Expose a public function to allow registering new types with
    /// custom names. With a `is_one_time_witness` call we make sure
    /// that for a single `T` this function can be called only once.
    public fun add_record<T: drop>(
        witness: T,
        name: String,
        ctx: &mut TxContext
    ) {
        // 这里将检查传入值是否为OTW
        // This call allows us to check whether type is an OTW;
        assert!(types::is_one_time_witness(&witness), ENotOneTimeWitness);

        // :)
        // Share the record for the world to see. :)
        transfer::share_object(UniqueTypeRecord<T> {
            id: object::new(ctx),
            name
        });
    }
}

/// 创建一个OTW的例子
/// Example of spawning an OTW.
module examples::my_otw {
    use std::string;
    use sui::tx_context::TxContext;
    use examples::one_time_witness_registry as registry;

    /// 使用模块名命名但是全部大写
    /// Type is named after the module but uppercased
    struct MY_OTW has drop {}

    /// 通过`init`函数的一个参数获取`MY_OTW`, 注意这并不是一个引用类型
    /// To get it, use the first argument of the module initializer.
    /// It is a full instance and not a reference type.
    fun init(witness: MY_OTW, ctx: &mut TxContext) {
        registry::add_record(
            witness, // here it goes <= 在这里使用
            string::utf8(b"My awesome record"),
            ctx
        )
    }
}

发布者(Publisher)

发布者(publisher)对象用于代表发布者的权限。这个对象本身并不表示任何特定的用例,它主要运用于只有两个函数:package::from_module<T>package::from_package<T>,允许检查类型 T 是否属于由“Publisher”对象创建的模块或包。

English Version

Publisher Object serves as a way to represent the publisher authority. The object itself does not imply any specific use case and has only two main functions: package::from_module<T> and package::from_package<T> which allow checking whether a type T belongs to a module or a package for which the Publisher object was created.

我们强烈建议为大多数定义新对象的包创建“Publisher”对象 - 这是设置“显示”以及允许该类型在“Kiosk”生态系统中进行交易的前提条件。

尽管 Publisher 本身是一种实用工具, 但是它实现了所有权证明("proof of ownership")的功能 例如在对象显示(Object Display)扮演了重要的角色.

English Version

We strongly advise to issue the Publisher object for most of the packages that define new Objects - it is required to set the "Display" as well as to allow the type to be traded in the "Kiosk" ecosystem.

Although Publisher itself is a utility, it enables the "proof of ownership" functionality, for example, it is crucial for the Object Display.

我们需要 OTW(One-Time-Witness)来设置发布者(publisher)以确保 Publisher 对象只在相应的模块中初始化一次(在包中可以初始化/创建多次), 同时创建 publisher 函数在发布模块的交易中调用。

English Version

To set up a Publisher, a One-Time-Witness (OTW) is required - this way we ensure the Publisher object is initialized only once for a specific module (but can be multiple for a package) as well as that the creation function is called in the publish transaction.

/// 在这个模块中,将定义一个OTW,并为模块发布者创建/声明一个`Publisher`对象
/// A simple package that defines an OTW and claims a `Publisher`
/// object for the sender.
module examples::owner {
    use sui::tx_context::TxContext;
    use sui::package;

    /// OTW 是一个与模块名相同(字母大写)并只含有`drop`的结构体
    /// OTW is a struct with only `drop` and is named
    /// after the module - but uppercased. See "One Time
    /// Witness" page for more details.
    struct OWNER has drop {}

    /// 定义另一个类型
    /// Some other type to use in a dummy check
    struct ThisType {}

    /// 在模块发布后,交易的发起者将获得一个`Publisher`对象。
    /// 可以用来在`Kiosk`系统中设置对象的显示或管理对象转移的规则。
    /// After the module is published, the sender will receive
    /// a `Publisher` object. Which can be used to set Display
    /// or manage the transfer policies in the `Kiosk` system.
    fun init(otw: OWNER, ctx: &mut TxContext) {
        package::claim_and_keep(otw, ctx)
    }
}

/// 一个利用“Publisher”对象为其所拥有的类型创建“TypeOwnerCap”对象的示例。
/// A module that utilizes the `Publisher` object to give a token
/// of appreciation and a `TypeOwnerCap` for the owned type.
module examples::type_owner {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;
    use sui::package::{Self, Publisher};

    /// 错误码0:当提供的类型无法与给定的`Publisher`匹配时触发
    /// Trying to claim ownership of a type with a wrong `Publisher`.
    const ENotOwner: u64 = 0;

    /// 当所给的类型(`T`)可以用给定的`Publisher`证明来源时,所创建的权限凭证
    /// A capability granted to those who want an "objective"
    /// confirmation of their ownership :)
    struct TypeOwnerCap<phantom T> has key, store {
        id: UID
    }

    /// 利用`Publisher`检查调用者是否拥有类型`T`
    /// Uses the `Publisher` object to check if the caller owns the type `T`.
    public fun prove_ownership<T>(
        publisher: &Publisher, ctx: &mut TxContext
    ): TypeOwnerCap<T> {
        assert!(package::from_package<T>(publisher), ENotOwner);
        TypeOwnerCap<T> { id: object::new(ctx) }
    }
}

对象显示(Object Display)

拥有 Publisher 对象的创作者或构建者可以使用 sui::display 模块来定义其对象的显示属性。请查看 Publisher 页面关于获取 Publisher 对象的方法。

Display<T> 是一个为类型 T 指定了一组命名的模板的对象(例如,对于类型 0x2::capy::Capy,显示对象将是 Display<0x2::capy::Capy>)。所有类型为 T 的对象都将通过匹配的 Display 定义在 Sui 全节点 RPC 中进行处理,并在查询对象时附加已处理的结果。

English Version

A creator or a builder who owns a Publisher object can use the sui::display module to define display properties for their objects. To get a Publisher object check out the Publisher page.

Display<T> is an object that specifies a set of named templates for the type T (for example, for a type 0x2::capy::Capy the display would be Display<0x2::capy::Capy>). All objects of the type T will be processed in the Sui Full Node RPC through the matching Display definition and will have processed result attached when an object is queried.

描述

Sui Object Display 是一个模板引擎,允许通过链上对类型显示进行配置以供生态系统在链下处理数据。它可以将模板中字符串替换为真实数据。

任意字段都可以被设置,对象的所有属性都可以通过 {property} 语法访问同时作为模板字符串的一部分插入其中(请参见示例)。

English Version

Description

Sui Object Display is a template engine which allows for on-chain display configuration for type to be handled off-chain by the ecosystem. It has the ability to use an object's data for substitution into a template string.

There's no limitation to what fields can be set, all object properties can be accessed via the {property} syntax and inserted as a part of the template string (see examples for the illustration).

示例

对于以下 Hero 模块,Display 将根据类型 Hero 的 name、id 和 img_url 属性而变化。在 init 函数中定义的模板可以表示为:

{
    "name": "{name}",
    "link": "https://sui-heroes.io/hero/{id}",
    "img_url": "ipfs://{img_url}",
    "description": "A true Hero of the Sui ecosystem!",
    "project_url": "https://sui-heroes.io",
    "creator": "Unknown Sui Fan"
}
English Version

Example

For the following Hero module, the Display would vary based on the "name", "id" and "img_url" properties of the type "Hero". The template defined in the init function can be represented as:

{
    "name": "{name}",
    "link": "https://sui-heroes.io/hero/{id}",
    "img_url": "ipfs://{img_url}",
    "description": "A true Hero of the Sui ecosystem!",
    "project_url": "https://sui-heroes.io",
    "creator": "Unknown Sui Fan"
}
/// 示例: "Sui Hero"藏品集
/// 任何人都可以创建属于自己的`Hero`, 在这个案例中我们将展示如何初始化`Publisher`,
/// 如何使用`Publisher`获取`Display<Hero>`对象--在生态系统中表示某一类型。
/// Example of an unlimited "Sui Hero" collection - anyone is free to
/// mint their Hero. Shows how to initialize the `Publisher` and how
/// to use it to get the `Display<Hero>` object - a way to describe a
/// type for the ecosystem.
module examples::my_hero {
    use sui::tx_context::{sender, TxContext};
    use std::string::{utf8, String};
    use sui::transfer;
    use sui::object::{Self, UID};

    // 创造者捆绑包:这两个包通常一起使用
    // The creator bundle: these two packages often go together.
    use sui::package;
    use sui::display;

    /// Hero结构体 - 用以代表数字藏品
    /// The Hero - an outstanding collection of digital art.
    struct Hero has key, store {
        id: UID,
        name: String,
        img_url: String,
    }

    /// 当前模块的 OTW
    /// One-Time-Witness for the module.
    struct MY_HERO has drop {}

    /// 在模块初始化函数中我们声明`Publisher`对象然后创建`Display`对象。
    /// `Display`将在初始化时设置多个项(之后可以更改),使用`update_version`发布。
    /// In the module initializer we claim the `Publisher` object
    /// to then create a `Display`. The `Display` is initialized with
    /// a set of fields (but can be modified later) and published via
    /// the `update_version` call.
    /// 
    /// `Display`对象的键值对可以在初始化时设置也可以在对象创建后更改
    /// Keys and values are set in the initializer but could also be
    /// set after publishing if a `Publisher` object was created.
    fun init(otw: MY_HERO, ctx: &mut TxContext) {
        let keys = vector[
            utf8(b"name"),
            utf8(b"link"),
            utf8(b"image_url"),
            utf8(b"description"),
            utf8(b"project_url"),
            utf8(b"creator"),
        ];

        let values = vector[
            // `name`对应`Hero.name`的值
            // For `name` we can use the `Hero.name` property
            utf8(b"{name}"),
            // `link`对应包括`Hero.id`的链接
            // For `link` we can build a URL using an `id` property
            utf8(b"https://sui-heroes.io/hero/{id}"),
            // `img_url`使用IPFS链接的模版
            // For `img_url` we use an IPFS template.
            utf8(b"ipfs://{img_url}"),
            // 一个针对所有`Hero`对象的描述
            // Description is static for all `Hero` objects.
            utf8(b"A true Hero of the Sui ecosystem!"),
            // 一个针对所有`Hero`藏品的网站链接
            // Project URL is usually static
            utf8(b"https://sui-heroes.io"),
            // 一个任意的项
            // Creator field can be any
            utf8(b"Unknown Sui Fan")
        ];

        // 为整个包创建`Publisher`对象
        // Claim the `Publisher` for the package!
        let publisher = package::claim(otw, ctx);

        // 为`Hero`类型创建`Display` 对象
        // Get a new `Display` object for the `Hero` type.
        let display = display::new_with_fields<Hero>(
            &publisher, keys, values, ctx
        );

        // 提交第一个版本`Display`
        // Commit first version of `Display` to apply changes.
        display::update_version(&mut display);

        transfer::public_transfer(publisher, sender(ctx));
        transfer::public_transfer(display, sender(ctx));
    }

    /// 任何人都可以创建`Hero`
    /// Anyone can mint their `Hero`!
    public fun mint(name: String, img_url: String, ctx: &mut TxContext): Hero {
        let id = object::new(ctx);
        Hero { id, name, img_url }
    }
}

方法描述

Display 对象是通过 display::new 调用创建的,可以在自定义函数(或模块初始化器)中执行,也作为可编程交易的一部分执行。

module sui::display {
    /// Get a new Display object for the `T`.
    /// Publisher must be the publisher of the T, `from_package`
    /// check is performed.
    public fun new<T>(pub: &Publisher): Display<T> { /* ... */ }
}

一旦 Display 对象生成,可以通过以下方法更改:

module sui::display {
    /// 同时更改多项内容
    /// Sets multiple fields at once
    public fun add_multiple(
        self: &mut Display,
        keys: vector<String>,
        values: vector<String
    ) { /* ... */ }

    /// 更改单项内容
    /// Edit a single field
    public fun edit(self: &mut Display, key: String, value: String) { /* ... */ }

    /// 从Display对象删除一个键值
    /// Remove a key from Display
    public fun remove(self: &mut Display, key: String ) { /* ... */ }
}

要使针对 Display 对象的更改生效需要调用 update_version 来触发一个事件(event),使得网络中各个完整节点监听到这个事件并获取类型 T 的新模板:

module sui::display {
    /// 更新Display对象的模板,并产生一个事件
    /// Update the version of Display and emit an event
    public fun update_version(self: &mut Display) { /* ... */ }
}
English Version

Methods description

Display is created via the display::new<T> call, which can be performed either in a custom function (or a module initializer) or as a part of a programmable transaction.

module sui::display {
    /// Get a new Display object for the `T`.
    /// Publisher must be the publisher of the T, `from_package`
    /// check is performed.
    public fun new<T>(pub: &Publisher): Display<T> { /* ... */ }
}

Once acquired, the Display can be modified:

module sui::display {
    /// Sets multiple fields at once
    public fun add_multiple(
        self: &mut Display,
        keys: vector<String>,
        values: vector<String
    ) { /* ... */ }

    /// Edit a single field
    public fun edit(self: &mut Display, key: String, value: String) { /* ... */ }

    /// Remove a key from Display
    public fun remove(self: &mut Display, key: String ) { /* ... */ }
}

To apply changes and set the Display for the T, one last call is required: update_version publishes version by emitting an event which Full Node listens to and uses to get a template for the type.

module sui::display {
    /// Update the version of Display and emit an event
    public fun update_version(self: &mut Display) { /* ... */ }
}

设计模式(Patterns)

在这个部分我们将介绍在 Move 中广泛使用的编程、设计模式;其中一些模式只在 Move 中存在。

English Version

This part covers the programming patterns that are widely used in Move; some of which can exist only in Move.

权限凭证(Capability)

权限凭证(capability)是一种与特定对象绑定授权的模式,最常见的便是TreasuryCap(定义在sui::coin中)。

English Version

Capability is a pattern that allows authorizing actions with an object. One of the most common capabilities is TreasuryCap (defined in sui::coin).

module examples::item {
    use sui::transfer;
    use sui::object::{Self, UID};
    use std::string::{Self, String};
    use sui::tx_context::{Self, TxContext};

    /// `AdminCap`类型与创建新`Item`对象的权限绑定
    /// Type that marks Capability to create new `Item`s.
    struct AdminCap has key { id: UID }

    /// 自定义的类似NFT的类型
    /// Custom NFT-like type.
    struct Item has key, store { id: UID, name: String }

    /// 模块的初始化函数只在模块初始化时调用一次,
    /// 因此只会存在一个`AdminCap`,并且这个权限凭证转移给了模块发布者
    /// Module initializer is called once on module publish.
    /// Here we create only one instance of `AdminCap` and send it to the publisher.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(AdminCap {
            id: object::new(ctx)
        }, tx_context::sender(ctx))
    }

    /// 这个入口函数只能被拥有`AdminCap`对象的发送者调用
    /// The entry function can not be called if `AdminCap` is not passed as
    /// the first argument. Hence only owner of the `AdminCap` can perform
    /// this action.
    public entry fun create_and_send(
        _: &AdminCap, name: vector<u8>, to: address, ctx: &mut TxContext
    ) {
        transfer::transfer(Item {
            id: object::new(ctx),
            name: string::utf8(name)
        }, to)
    }
}

见证(Witness)

见证(witness)模式被用于验证某一类型的所有权。为此,需要传递一个拥有 drop 的类型实例。Coin 就使用了这种模式。

English Version

Witness is a pattern that is used for confirming the ownership of a type. To do so, one passes a drop instance of a type. Coin relies on this implementation.

/// 在这个模块中定义了一个泛型类型`Guardian<T>`,并且只可使用witness创建
/// Module that defines a generic type `Guardian<T>` which can only be
/// instantiated with a witness.
module examples::guardian {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    /// 虚参数`T`只可被`create_guardian`初始化, 同时这个类型需要具备`drop`修饰符
    /// Phantom parameter T can only be initialized in the `create_guardian`
    /// function. But the types passed here must have `drop`.
    struct Guardian<phantom T: drop> has key, store {
        id: UID
    }

    /// 第一个参数是一个类型T具有`drop`能力的示例,它会在接受后被丢弃
    /// The first argument of this function is an actual instance of the
    /// type T with `drop` ability. It is dropped as soon as received.
    public fun create_guardian<T: drop>(
        _witness: T, ctx: &mut TxContext
    ): Guardian<T> {
        Guardian { id: object::new(ctx) }
    }
}

/// 一个自定义的模块,用来使用`guardian`模块
/// Custom module that makes use of the `guardian`.
module examples::peace_guardian {
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // 引入`guardian`依赖
    // Use the `guardian` as a dependency.
    use 0x0::guardian;

    /// `PEACE`类型只被使用一次
    /// This type is intended to be used only once.
    struct PEACE has drop {}

    /// 模块初始化函数可以确保其中的代码只被执行一次,这也是见证人模式最常使用方法
    /// Module initializer is the best way to ensure that the
    /// code is called only once. With `Witness` pattern it is
    /// often the best practice.
    fun init(ctx: &mut TxContext) {
        transfer::public_transfer(
            guardian::create_guardian(PEACE {}, ctx),
            tx_context::sender(ctx)
        )
    }
}

见证模式被应用于以下例子中:

可转移见证(Transferable Witness)

/// 可转移见证模式(transferable witness)基于权限凭证(Capability)和见证(Witness)模式。
/// 因为Witness需要小心处理,因此只有经过授权的用户(理想情况下只使用一次)才能生成它。
/// 但是,在某些情况中需要模块X对类型进行授权以便在另一个模块Y中使用,或者需要一段时间后使用。
/// This pattern is based on a combination of two others: Capability and a Witness.
/// Since Witness is something to be careful with, spawning it should be allowed
/// only to authorized users (ideally only once). But some scenarios require
/// type authorization by module X to be used in another module Y. Or, possibly,
/// there's a case where authorization should be performed after some time.
///
/// 在这些特殊的场景下,可存储的见证(storable witness)就排上了用场。
/// For these rather rare scerarios, a storable witness is a perfect solution.
module examples::transferable_witness {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    /// Witness具有store属性因此可以将其存储在包装结构体中
    /// Witness now has a `store` that allows us to store it inside a wrapper.
    struct WITNESS has store, drop {}

    /// `WitnessCarrier`是`WITNESS`的封装容器,只能获得一次`WITNESS`
    /// Carries the witness type. Can be used only once to get a Witness.
    struct WitnessCarrier has key { id: UID, witness: WITNESS }

    /// 将`WitnessCarrier`发送给模块的发布者
    /// Send a `WitnessCarrier` to the module publisher.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(
            WitnessCarrier { id: object::new(ctx), witness: WITNESS {} },
            tx_context::sender(ctx)
        )
    }

    /// 解开包装获得`WITNESS`
    /// Unwrap a carrier and get the inner WITNESS type.
    public fun get_witness(carrier: WitnessCarrier): WITNESS {
        let WitnessCarrier { id, witness } = carrier;
        object::delete(id);
        witness
    }
}

烫手山芋(Hot Potato)

烫手山芋(Hot Potato)是一个没有任何能力修饰符的结构体,因此它只能在其模块中被打包和解包。如果函数 A 返回这样一个结构,而函数 B 消耗它,那么我们必须在函数 A 之后调用函数 B。

English Version

Hot Potato is a name for a struct that has no abilities, hence it can only be packed and unpacked in its module. In this struct, you must call function B after function A in the case where function A returns a potato and function B consumes it.

module examples::trade_in {
    use sui::transfer;
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::object::{Self, UID};
    use sui::tx_context::{TxContext};

    /// 第一种手机的价格
    /// Price for the first phone model in series
    const MODEL_ONE_PRICE: u64 = 10000;

    /// 第二种手机的价格
    /// Price for the second phone model
    const MODEL_TWO_PRICE: u64 = 20000;

    /// 错误码 1:购买的手机类型不存在
    /// For when someone tries to purchase non-existing model
    const EWrongModel: u64 = 1;

    /// 错误码 2:支付金额不足
    /// For when paid amount does not match the price
    const EIncorrectAmount: u64 = 2;

    /// Phone: 可以被购买或者以旧换新
    /// A phone; can be purchased or traded in for a newer model
    struct Phone has key, store { id: UID, model: u8 }

    /// Receipt: 可以直接支付或者接受以旧换新,不可以被储存,拥有或者丢弃, 
    /// 必须在`trade_in` 或者 `pay_full` 方法中被消耗
    /// Payable receipt. Has to be paid directly or paid with a trade-in option.
    /// Cannot be stored, owned or dropped - has to be used to select one of the
    /// options for payment: `trade_in` or `pay_full`.
    struct Receipt { price: u64 }

    /// 购买手机,返回的`Receipt`必须在`trade_in` 或者 `pay_full`中被消耗。
    /// Get a phone, pay later.
    /// Receipt has to be passed into one of the functions that accept it:
    ///  in this case it's `pay_full` or `trade_in`.
    public fun buy_phone(model: u8, ctx: &mut TxContext): (Phone, Receipt) {
        assert!(model == 1 || model == 2, EWrongModel);

        let price = if (model == 1) MODEL_ONE_PRICE else MODEL_TWO_PRICE;

        (
            Phone { id: object::new(ctx), model },
            Receipt { price }
        )
    }

    /// 全款支付,获得`Phone`对象,同时`Receipt`被消耗
    /// Pay the full price for the phone and consume the `Receipt`.
    public fun pay_full(receipt: Receipt, payment: Coin<SUI>) {
        let Receipt { price } = receipt;
        assert!(coin::value(&payment) == price, EIncorrectAmount);

        // for simplicity's sake transfer directly to @examples account
        transfer::public_transfer(payment, @examples);
    }

    /// 以旧换新,传入一个已有的`Phone`对象,获得新的`Phone`对象,同时`Receipt`被消耗
    /// Give back an old phone and get 50% of its price as a discount for the new one.
    public fun trade_in(receipt: Receipt, old_phone: Phone, payment: Coin<SUI>) {
        let Receipt { price } = receipt;
        let tradein_price = if (old_phone.model == 1) MODEL_ONE_PRICE else MODEL_TWO_PRICE;
        let to_pay = price - (tradein_price / 2);

        assert!(coin::value(&payment) == to_pay, EIncorrectAmount);

        transfer::public_transfer(old_phone, @examples);
        transfer::public_transfer(payment, @examples);
    }
}

烫手山芋模式被应用于以下例子中:

ID 指针(ID Pointer)

ID指针模式旨在将主要数据(一个对象)与其访问器/权限分离。这种模式可以用于几个不同的方向:

  • 为共享对象提供可转让(转移)的功能(例如,利用 TransferCap 权限更改共享对象的 “owner” 字段)
  • 将动态数据与静态数据分开(例如,单个 NFT 及其作品集的信息)
  • 避免不必要的类型链接(以及见证的要求)(例如,流动性池中的 LP 代币)
English Version

ID Pointer is a technique that separates the main data (an object) and its accessors / capabilities by linking the latter to the original. There's a few different directions in which this pattern can be used:

  • issuing transferable capabilities for shared objects (for example, a TransferCap that changes 'owner' field of a shared object)
  • splitting dynamic data and static (for example, an NFT and its Collection information)
  • avoiding unnecessary type linking (and witness requirement) in generic applications (LP token for a LiquidityPool)
/// 在这个例子中将实现一个简单的`Lock`和`Key`的机制(锁和钥匙)
/// `Lock<T>`是一个共享对象可以包含任意对象,`Key`是一个被拥有的对象,
/// 需要`Key`才能访问`Lock`的内容
/// This example implements a simple `Lock` and `Key` mechanics
/// on Sui where `Lock<T>` is a shared object that can contain any object,
/// and `Key` is an owned object which is required to get access to the
/// contents of the lock.
///
/// `Key`通过`ID`字段与`Lock`相关联。这样的检查允许链下发现目标,同时将动态可转让的功能与“静态”内容分离。
/// 另一个好处是目标资产始终可以被发现,而其`Key`可以被包装到另一个对象中(例如,市场列表)。
/// `Key` is linked to its `Lock` using an `ID` field. This check allows
/// off-chain discovery of the target as well as splits the dynamic
/// transferable capability and the 'static' contents. Another benefit of
/// this approach is that the target asset is always discoverable while its
/// `Key` can be wrapped into another object (eg a marketplace listing).
module examples::lock_and_key {
    use sui::object::{Self, ID, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use std::option::{Self, Option};

    /// 错误码 0: `Lock`为空
    /// Lock is empty, nothing to take.
    const ELockIsEmpty: u64 = 0;

    /// 错误码 1: `Lock`与`Key`不匹配
    /// Key does not match the Lock.
    const EKeyMismatch: u64 = 1;

    /// 错误码 2: `Lock`已被使用
    /// Lock already contains something.
    const ELockIsFull: u64 = 2;

    /// `Lock`容器可以存放任意内容
    /// Lock that stores any content inside it.
    struct Lock<T: store + key> has key {
        id: UID,
        locked: Option<T>
    }

    /// `Key`对象伴随`Lock`一同生成,它是可以变更所有权的,同时可以打开`Lock`
    /// A key that is created with a Lock; is transferable
    /// and contains all the needed information to open the Lock.
    struct Key<phantom T: store + key> has key, store {
        id: UID,
        for: ID,
    }

    /// 返回`Key`对象所对应的`Lock`对象的ID
    /// Returns an ID of a Lock for a given Key.
    public fun key_for<T: store + key>(key: &Key<T>): ID {
        key.for
    }

    /// 在`Lock`中保存一些内容并设置为共享对象。生成对应的`Key`对象。
    /// 例如我们可以利用`Lock`保存一些SUI代币
    /// Lock some content inside a shared object. A Key is created and is
    /// sent to the transaction sender. For example, we could turn the
    /// lock into a treasure chest by locking some `Coin<SUI>` inside.
    ///
    /// 交易发送者获得`Key`
    /// Sender gets the `Key` to this `Lock`.
    public entry fun create<T: store + key>(obj: T, ctx: &mut TxContext) {
        let id = object::new(ctx);
        let for = object::uid_to_inner(&id);

        transfer::share_object(Lock<T> {
            id,
            locked: option::some(obj),
        });

        transfer::transfer(Key<T> {
            for,
            id: object::new(ctx)
        }, tx_context::sender(ctx));
    }

    /// 将某种对象锁在`Lock`中,当`Key`不匹配或者`Lock`中已经保存了内容时报错。
    /// Lock something inside a shared object using a Key. Aborts if
    /// lock is not empty or if key doesn't match the lock.
    public entry fun lock<T: store + key>(
        obj: T,
        lock: &mut Lock<T>,
        key: &Key<T>,
    ) {
        assert!(option::is_none(&lock.locked), ELockIsFull);
        assert!(&key.for == object::borrow_id(lock), EKeyMismatch);

        option::fill(&mut lock.locked, obj);
    }

    /// 利用`Key`解锁`Lock`并获得保存的对象。
    /// 当`Key`不匹配或者`Lock`中无内容时报错。
    /// Unlock the Lock with a Key and access its contents.
    /// Can only be called if both conditions are met:
    /// - key matches the lock
    /// - lock is not empty
    public fun unlock<T: store + key>(
        lock: &mut Lock<T>,
        key: &Key<T>,
    ): T {
        assert!(option::is_some(&lock.locked), ELockIsEmpty);
        assert!(&key.for == object::borrow_id(lock), EKeyMismatch);

        option::extract(&mut lock.locked)
    }

    /// 利用`Key`解锁`Lock`并获得保存的对象, 将保存的对象转移给交易发起者
    /// Unlock the Lock and transfer its contents to the transaction sender.
    public fun take<T: store + key>(
        lock: &mut Lock<T>,
        key: &Key<T>,
        ctx: &mut TxContext,
    ) {
        transfer::public_transfer(unlock(lock, key), tx_context::sender(ctx))
    }
}

ID 指针模式被应用于以下例子中:

示例(Samples)

这个章节包含了常见的区块链用例,持续更新中~

English Version

This section contains a growing collection of ready-to-go samples for common blockchain use cases.

创建 NFT(Make an NFT)

在 Sui 中,一切都可以被当做 NFT,因为对象(Object)是独一无二,可被拥有的。因此创建 NFT 只需要一个新的类型就可以。

English Version

In Sui, everything is an NFT - Objects are unique, non-fungible and owned. So technically, a simple type publishing is enough.

module examples::devnet_nft {
    use sui::url::{Self, Url};
    use std::string;
    use sui::object::{Self, ID, UID};
    use sui::event;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// 一个允许任何人铸造NFT的示例
    /// An example NFT that can be minted by anybody
    struct DevNetNFT has key, store {
        id: UID,
        /// Name for the token 代币(NFT)名
        name: string::String,
        /// Description of the token 代币(NFT)描述
        description: string::String,
        /// URL for the token 代币(NFT)链接
        url: Url,
        // TODO: allow custom attributes 
    }

    // ===== Events ===== 事件

    struct NFTMinted has copy, drop {
        // The Object ID of the NFT 新铸造的NFT的ID
        object_id: ID,
        // The creator of the NFT 新铸造的NFT的创造者
        creator: address,
        // The name of the NFT 新铸造的NFT的名
        name: string::String,
    }

    // ===== Public view functions ===== 公共视图函数

    /// 获取NFT的名称
    /// Get the NFT's `name` 
    public fun name(nft: &DevNetNFT): &string::String {
        &nft.name
    }

    /// 获取NFT的介绍
    /// Get the NFT's `description`
    public fun description(nft: &DevNetNFT): &string::String {
        &nft.description
    }

    /// 获取NFT的链接
    /// Get the NFT's `url`
    public fun url(nft: &DevNetNFT): &Url {
        &nft.url
    }

    // ===== Entrypoints ===== 入口函数

    /// 创建新的NFT
    /// Create a new devnet_nft
    public entry fun mint_to_sender(
        name: vector<u8>,
        description: vector<u8>,
        url: vector<u8>,
        ctx: &mut TxContext
    ) {
        let sender = tx_context::sender(ctx);
        let nft = DevNetNFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            url: url::new_unsafe_from_bytes(url)
        };

        event::emit(NFTMinted {
            object_id: object::id(&nft),
            creator: sender,
            name: nft.name,
        });

        transfer::public_transfer(nft, sender);
    }

    /// 转移NFT给新的所有者
    /// Transfer `nft` to `recipient`
    public entry fun transfer(
        nft: DevNetNFT, recipient: address, _: &mut TxContext
    ) {
        transfer::public_transfer(nft, recipient)
    }

    /// 更新NFT的介绍
    /// Update the `description` of `nft` to `new_description`
    public entry fun update_description(
        nft: &mut DevNetNFT,
        new_description: vector<u8>,
        _: &mut TxContext
    ) {
        nft.description = string::utf8(new_description)
    }

    /// 永久删除NFT
    /// Permanently delete `nft`
    public entry fun burn(nft: DevNetNFT, _: &mut TxContext) {
        let DevNetNFT { id, name: _, description: _, url: _ } = nft;
        object::delete(id)
    }
}

创建 ERC20 代币(Create a Coin (ERC20))

在 Sui 上发布一种代币几乎和发布一个新类型一样简单。但是,不同的是,它需要使用一次性见证(One Time Witness)。

English Version

Publishing a coin is Sui is almost as simple as publishing a new type. However it is a bit tricky as it requires using a One Time Witness.

module examples::mycoin {
    use std::option;
    use sui::coin;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// 这个结构体代表代币类型,每种代币都需要一个类型:`Coin<package_object::mycoin::MYCOIN>`
    /// 确保结构体名与模块名匹配
    /// The type identifier of coin. The coin will have a type
    /// tag of kind: `Coin<package_object::mycoin::MYCOIN>`
    /// Make sure that the name of the type matches the module's name.
    struct MYCOIN has drop {}

    /// 模块初始化函数在模块发布时被调用。
    /// `TreasuryCap`会被发送给模块的发布者,因此发布者可以控制代币铸造和销毁。
    /// Module initializer is called once on module publish. A treasury
    /// cap is sent to the publisher, who then controls minting and burning
    fun init(witness: MYCOIN, ctx: &mut TxContext) {
        let (treasury, metadata) = coin::create_currency(witness, 6, b"MYCOIN", b"", b"", option::none(), ctx);
        transfer::public_freeze_object(metadata);
        transfer::public_transfer(treasury, tx_context::sender(ctx))
    }
}

Coin<T> 是 Sui 上代币的通用实现。TreasuryCap 的所有者可以控制硬币的铸造和销毁,通过与 sui::coin::Coin 交互,并使用 TreasuryCap 对象作为授权。

English Version

The Coin<T> is a generic implementation of a Coin on Sui. Owner of the TreasuryCap gets control over the minting and burning of coins. Further transactions can be sent directly to the sui::coin::Coin with TreasuryCap object as authorization.

其他资源(Additional Resources)

想要了解更多关于 Sui Move 的信息,请查看以下链接: