Move by Example

Welcome to the companion book to docs.sui.io. There we describe the Move on Sui and explain how to use it to write smart contracts.

Instead, this site builds upon the application 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.

All code samples in this book are written with the assumption that you use Move on Sui, 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.

Basics

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

Move.toml

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 @examples will be treated as a 0x0 address)
[package]
name = "examples"
edition = "2024.beta" 
# license = ""           # e.g., "MIT", "GPL", "Apache 2.0"
# authors = ["..."]      # e.g., ["Joe Smith ([email protected])", "John Snow ([email protected])"]

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

# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`.
# Revision can be a branch, a tag, and a commit hash.
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }

# For local dependencies use `local = path`. Path is relative to the package root
# Local = { local = "../path/to" }

# To resolve a version conflict and force a specific version for dependency
# override use `override = true`
# Override = { local = "../conflicting/version", override = true }

[addresses]
examples = "0x0"

# Named addresses will be accessible in Move as `@name`. They're also exported:
# for example, `std = "0x1"` is exported by the Standard Library.
# alice = "0xA11CE"

[dev-dependencies]
# The dev-dependencies section allows overriding dependencies for `--test` and
# `--dev` modes. You can introduce test-only dependencies here.
# Local = { local = "../path/to/dev-build" }

[dev-addresses]
# The dev-addresses section allows overwriting named addresses for the `--test`
# and `--dev` modes.
# alice = "0xB0B"

Stdlib and Sui Framework

The Sui dependency adds two addresses to the package:

  • std = 0x1 - address of the standard library
  • sui = 0x2 - address of the Sui Framework

Learn more

Init Function

Init is a special function which gets called when a module is published. It is guaranteed that it will never be called again. It can take the &mut TxContext as the last argument and a One-Time-Witness (optional) as the first argument.

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

For example:

module examples::init_function {
    /// The one of a kind - created in the module initializer.
    public struct CreatorCap has key {
        id: UID
    }

    /// 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
    /// `CreatorCap` struct.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(CreatorCap {
            id: object::new(ctx),
        }, ctx.sender())
    }
}

Learn more

Entry Function

An entry function visibility modifier restricts calling a function from another package but allows direct calls in a transaction block. Entry functions can't return a value that doesn't have drop ability.

module examples::object {
    /// An object is a struct with `key` ability and `id: UID` field.
    public struct Object has key {
        id: UID
    }

    /// 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) }
    }

    /// 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)
    }
}

Entry functions can be public, and that would make them callable from other modules. However, the return value restrictions still apply.

Learn more

Strings

Move does not have a native type for strings, but it has a handy wrapper! Sui supports String type as a transaction argument (encoded as vector<u8> on the client).

module examples::strings {
    use std::string::String;

    /// A dummy Object that holds a String type
    public struct Name has key, store {
        id: UID,
        /// Here it is - the String type
        name: String
    }

    /// Create a name Object by passing raw bytes
    public fun mint_name(name: String, ctx: &mut TxContext): Name {
        Name { id: object::new(ctx), name }
    }
}

String operations

To create a string from a vector of bytes, use string::utf8:

module examples::string_operations {
    use std::string::String;

    /// Constants can't be Strings; and they're not public.
    const HELLO_WORLD: vector<u8> = b"Hello, World!";

    /// But a function can pack a string and make it accessible.
    /// Note: `string::utf8` fails if the string is not valid UTF-8.
    public fun hello_world(): String {
        HELLO_WORLD.to_string()
    }

    /// Checks if it's a valid UTF-8 String and returns an option
    public fun new_string_safe(bytes: vector<u8>): Option<String> {
        bytes.try_to_string()
    }
}

Shared Object

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

When sharing an object defined in this module, use transfer::share_object; for types defined in other modules, use transfer::public_share_object. Inside a transaction block, only the latter is allowed.

/// 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::sui::SUI;
    use sui::coin::Coin;
    use sui::balance::{Self, Balance};

    /// For when Coin balance is too low.
    const ENotEnough: u64 = 0;

    /// Capability that grants an owner the right to collect profits.
    public struct ShopOwnerCap has key { id: UID }

    /// A purchasable Donut. For simplicity's sake we ignore implementation.
    public struct Donut has key { id: UID }

    /// A shared object. `key` ability is required.
    public 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.
    ///
    /// To share an object `transfer::share_object` is used.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(ShopOwnerCap {
            id: object::new(ctx)
        }, ctx.sender());

        // Share the object to make it accessible to everyone!
        transfer::share_object(DonutShop {
            id: object::new(ctx),
            price: 1000,
            balance: balance::zero()
        })
    }

    /// Entry function available to everyone who owns a Coin.
    public fun buy_donut(
        shop: &mut DonutShop, payment: &mut Coin<SUI>, ctx: &mut TxContext
    ) {
        assert!(payment.value() >= shop.price, ENotEnough);

        // Take amount = `shop.price` from Coin<SUI>
        let paid = payment.balance_mut().split(shop.price);

        // Put the coin to the Shop's balance
        shop.balance.join(paid);

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

    /// Consume donut and get nothing...
    public fun eat_donut(d: Donut) {
        let Donut { id } = d;
        id.delete();
    }

    /// Take coin from `DonutShop` and transfer it to tx sender.
    /// Requires authorization with `ShopOwnerCap`.
    public fun collect_profits(
        _: &ShopOwnerCap, shop: &mut DonutShop, ctx: &mut TxContext
    ): Coin<SUI> {
        let amount = shop.balance.value();
        shop.balance.split(amount).into_coin(ctx)
    }
}

Transfer

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

/// A freely transfererrable Wrapper for custom data.
module examples::wrapper {
    /// An object with `store` can be transferred in any
    /// module without a custom transfer implementation.
    public struct Wrapper<T: store> has key, store {
        id: UID,
        contents: T
    }

    /// 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),
        }
    }

    /// Destroy `Wrapper` and get T.
    public fun destroy<T: store> (c: Wrapper<T>): T {
        let Wrapper { id, contents } = c;
        id.delete();
        contents
    }
}

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

    // using Wrapper functionality
    use examples::wrapper::{Self, Wrapper};

    /// Profile information, not an object, can be wrapped
    /// into a transferable container
    public struct ProfileInfo has store {
        name: String,
        url: Url
    }

    /// Read `name` field from `ProfileInfo`.
    public fun name(info: &ProfileInfo): &String {
        &info.name
    }

    /// Read `url` field from `ProfileInfo`.
    public fun url(info: &ProfileInfo): &Url {
        &info.url
    }

    /// Creates new `ProfileInfo` and wraps into `Wrapper`.
    /// Then transfers to sender.
    public fun create_profile(
        name: vector<u8>, url: vector<u8>, ctx: &mut TxContext
    ): Wrapper<ProfileInfo> {
        // create a new container and wrap ProfileInfo into it
        // return the container to the caller
        wrapper::create(ProfileInfo {
            name: name.to_string(),
            url: url::new_unsafe_from_bytes(url)
        }, ctx)
    }
}

Custom transfer

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::balance::{Self, Balance};
    use sui::coin::Coin;
    use sui::sui::SUI;

    /// For when paid amount is not equal to the transfer price.
    const EWrongAmount: u64 = 0;

    /// A Capability that allows bearer to create new `TitleDeed`s.
    public struct GovernmentCapability has key { id: UID }

    /// An object that marks a property ownership. Can only be issued
    /// by an authority.
    public struct TitleDeed has key {
        id: UID,
        // ... some additional fields
    }

    /// A centralized registry that approves property ownership
    /// transfers and collects fees.
    public struct LandRegistry has key {
        id: UID,
        balance: Balance<SUI>,
        fee: u64
    }

    /// Create a `LandRegistry` on module init.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(GovernmentCapability {
            id: object::new(ctx)
        }, ctx.sender());

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

    /// Create `TitleDeed` and transfer it to the property owner.
    /// Only owner of the `GovernmentCapability` can perform this action.
    public fun issue_title_deed(
        _: &GovernmentCapability,
        `for`: address,
        ctx: &mut TxContext
    ) {
        transfer::transfer(TitleDeed {
            id: object::new(ctx)
        }, `for`)
    }

    /// 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 fun transfer_ownership(
        registry: &mut LandRegistry,
        paper: TitleDeed,
        fee: Coin<SUI>,
        to: address,
    ) {
        assert!(&fee.value() == registry.fee, EWrongAmount);

        // add a payment to the LandRegistry balance
        registry.balance.join(fee.into_balance());

        // finally call the transfer function
        transfer::transfer(paper, to)
    }
}

Learn more

Clock

Sui has built-in support for a clock. The clock is a special object that can be used to get the current time. It is a shared object, and can be accessed by anyone.

Clock has a reserved address 0x6. While being a shared object, it can't be accessed mutably, and a transaction attempting to do so will fail.

module examples::clock {
    // Import the `clock` module and the `Clock` type.
    use sui::clock::Clock;

    /// A dummy object that hold the time it was created.
    /// What if we could sell a timestamp...?
    public struct Timestamp has key, store {
        id: UID,
        timestamp: u64,
    }

    /// Creates a new `Timestamp` Object using the `Clock`.
    public fun new(clock: &Clock, ctx: &mut TxContext): Timestamp {
        // The `timestamp_ms` is the main function of the `Clock` module.
        let timestamp = clock.timestamp_ms();

        Timestamp { timestamp, id: object::new(ctx) }
    }
}

Learn more

Events

Events are the main way to track actions on chain.

/// Extended example of a shared object. Now with addition of events!
module examples::donuts_with_events {
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};

    // This is the only dependency you need for events.
    use sui::event;

    /// For when Coin balance is too low.
    const ENotEnough: u64 = 0;

    /// Capability that grants an owner the right to collect profits.
    public struct ShopOwnerCap has key { id: UID }

    /// A purchasable Donut. For simplicity's sake we ignore implementation.
    public struct Donut has key { id: UID }

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

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

    /// For when someone has purchased a donut.
    public struct DonutBought has copy, drop {
        id: ID
    }

    /// For when DonutShop owner has collected profits.
    public struct ProfitsCollected has copy, drop {
        amount: u64
    }

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

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

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

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

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

        shop.balance.join(paid);

        // Emit the event using future object's ID.
        event::emit(DonutBought { id: id.to_inner() });
        transfer::transfer(Donut { id }, ctx.sender())
    }

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

    /// Take coin from `DonutShop` and transfer it to tx sender.
    /// Requires authorization with `ShopOwnerCap`.
    public fun collect_profits(
        _: &ShopOwnerCap, shop: &mut DonutShop, ctx: &mut TxContext
    ): Coin<SUI> {
        let amount = shop.balance.value();

        // simply create new type instance and emit it
        event::emit(ProfitsCollected { amount });
        shop.balance.split(amount).into_coin(ctx)
    }
}

Learn more

One Time Witness

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.

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
    public struct MYCOIN has drop {}

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

Example which illustrates how OTW could be used:

/// This example illustrates how One Time Witness works.
///
/// 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
/// - named after the module (uppercased)
/// - cannot be packed manually
/// - has a `drop` ability
module examples::one_time_witness_registry {
    use std::string::String;

    // This dependency allows us to check whether type
    // is a one-time witness (OTW)
    use sui::types;

    /// 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.
    public struct UniqueTypeRecord<phantom T> has key {
        id: UID,
        name: String
    }

    /// 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
    ) {
        // 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
        });
    }
}

/// Example of spawning an OTW.
module examples::my_otw {
    use examples::one_time_witness_registry as registry;

    /// Type is named after the module but uppercased
    public struct MY_OTW has drop {}

    /// 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
            b"My awesome record".to_string(),
            ctx
        )
    }
}

Publisher

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.

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.

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.

/// A simple package that defines an OTW and claims a `Publisher`
/// object for the sender.
module examples::owner {
    use sui::package;

    /// OTW is a struct with only `drop` and is named
    /// after the module - but uppercased. See "One Time
    /// Witness" page for more details.
    public struct OWNER has drop {}

    /// Some other type to use in a dummy check
    public struct ThisType {}

    /// 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)
    }
}

/// 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::package::{Self, Publisher};

    /// Trying to claim ownership of a type with a wrong `Publisher`.
    const ENotOwner: u64 = 0;

    /// A capability granted to those who want an "objective"
    /// confirmation of their ownership :)
    public struct TypeOwnerCap<phantom T> has key, store {
        id: UID
    }

    /// 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

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.

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).

Example

For the following Hero module, the Display would vary based on the "name", "id" and "image_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}",
    "image_url": "ipfs://{img_url}",
    "description": "A true Hero of the Sui ecosystem!",
    "project_url": "https://sui-heroes.io",
    "creator": "Unknown Sui Fan"
}
/// 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 std::string::String;

    // The creator bundle: these two packages often go together.
    use sui::package;
    use sui::display;

    /// The Hero - an outstanding collection of digital art.
    public struct Hero has key, store {
        id: UID,
        name: String,
        img_url: String,
    }

    /// One-Time-Witness for the module.
    public struct MY_HERO has drop {}

    /// 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.
    ///
    /// 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[
            b"name".to_string(),
            b"link".to_string(),
            b"image_url".to_string(),
            b"description".to_string(),
            b"project_url".to_string(),
            b"creator".to_string(),
        ];

        let values = vector[
            // For `name` we can use the `Hero.name` property
            b"{name}".to_string(),
            // For `link` we can build a URL using an `id` property
            b"https://sui-heroes.io/hero/{id}".to_string(),
            // For `image_url` we use an IPFS template + `img_url` property.
            b"ipfs://{img_url}".to_string(),
            // Description is static for all `Hero` objects.
            b"A true Hero of the Sui ecosystem!".to_string(),
            // Project URL is usually static
            b"https://sui-heroes.io".to_string(),
            // Creator field can be any
            b"Unknown Sui Fan".to_string()
        ];

        // Claim the `Publisher` for the package!
        let publisher = package::claim(otw, ctx);

        // Get a new `Display` object for the `Hero` type.
        let mut display = display::new_with_fields<Hero>(
            &publisher, keys, values, ctx
        );

        // Commit first version of `Display` to apply changes.
        display.update_version();

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

    /// 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 }
    }
}

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) { /* ... */ }
}

Learn more

Patterns

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

Views and Accessors

Struct fields are private to the module. To access them from outside the module, you must define an accessor function. The convention is to name the function after the field. It's up to the developer to decide which of the fields should be accessible from outside the module.

/// This module implements a simple object that represents a user record. All
/// of the fields of the record can be read by using the getter functions. The
/// email and phone fields can be changed by using the mutable access functions.
module examples::record {
    use std::string::String;

    /// A single record of a user. An Object!
    public struct UserRecord has key, store {
        id: UID,
        name: String,
        age: u8,
        email: String,
        phone: String,
    }

    /// Creates a new user record
    public fun new(
        name: String, age: u8, email: String, phone: String, ctx: &mut TxContext
    ): UserRecord {
        UserRecord {
            id: object::new(ctx),
            name, age, email, phone,
        }
    }

    // === Mutable Accessors ===

    /// Return a mutable reference to the user's email.
    ///
    /// Hint: the `_mut` suffix on the function name marks this function as
    /// a mutable accessor.
    public fun email_mut(self: &mut UserRecord): &mut String { &mut self.email }

    /// Return a mutable reference to the user's phone
    public fun phone_mut(self: &mut UserRecord): &mut String { &mut self.phone }

    // === Views ===

    /// Returns the user's name.
    /// Hint: view functions match property names.
    public fun name(self: &UserRecord): String { self.name }

    /// Returns the user's age
    public fun age(self: &UserRecord): u8 { self.age }

    /// Returns the user's email
    public fun email(self: &UserRecord): String { self.email }

    /// Returns the user's phone
    public fun phone(self: &UserRecord): String { self.phone }
}

This pattern is used in almost every application of Move.

Capability

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 std::string::String;

    /// Type that marks Capability to create new `Item`s.
    public struct AdminCap has key { id: UID }

    /// Custom NFT-like type.
    public struct Item has key, store { id: UID, name: String }

    /// 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)
        }, ctx.sender())
    }

    /// 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 fun create_and_send(
        _: &AdminCap, name: vector<u8>, to: address, ctx: &mut TxContext
    ) {
        transfer::transfer(Item {
            id: object::new(ctx),
            name: name.to_string()
        }, to)
    }
}

Witness

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.

/// Module that defines a generic type `Guardian<T>` which can only be
/// instantiated with a witness.
module examples::guardian {
    /// Phantom parameter T can only be initialized in the `create_guardian`
    /// function. But the types passed here must have `drop`.
    public struct Guardian<phantom T: drop> has key, store {
        id: UID
    }

    /// 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) }
    }
}

/// Custom module that makes use of the `guardian`.
module examples::peace_guardian {
    // Use the `guardian` as a dependency.
    use examples::guardian;

    /// This type is intended to be used only once.
    public 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),
            ctx.sender()
        )
    }
}

This pattern is used in these examples:

Transferable Witness

/// 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.
///
/// For these rather rare scerarios, a storable witness is a perfect solution.
module examples::transferable_witness {
    /// Witness now has a `store` that allows us to store it inside a wrapper.
    public struct WITNESS has store, drop {}

    /// Carries the witness type. Can be used only once to get a Witness.
    public struct WitnessCarrier has key { id: UID, witness: WITNESS }

    /// Send a `WitnessCarrier` to the module publisher.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(
            WitnessCarrier { id: object::new(ctx), witness: WITNESS {} },
            ctx.sender()
        )
    }

    /// Unwrap a carrier and get the inner WITNESS type.
    public fun get_witness(carrier: WitnessCarrier): WITNESS {
        let WitnessCarrier { id, witness } = carrier;
        id.delete();
        witness
    }
}

Hot Potato

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::sui::SUI;
    use sui::coin::Coin;

    /// 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;

    /// For when someone tries to purchase non-existing model
    const EWrongModel: u64 = 1;

    /// For when paid amount does not match the price
    const EIncorrectAmount: u64 = 2;

    /// A phone; can be purchased or traded in for a newer model
    public struct Phone has key, store { id: UID, model: u8 }

    /// 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`.
    public struct Receipt { price: u64 }

    /// 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 }
        )
    }

    /// 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!(payment.value() == price, EIncorrectAmount);

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

    /// 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!(payment.value() == to_pay, EIncorrectAmount);

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

This pattern is used in these examples:

ID Pointer

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)
/// 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` 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 {
    /// Lock is empty, nothing to take.
    const ELockIsEmpty: u64 = 0;

    /// Key does not match the Lock.
    const EKeyMismatch: u64 = 1;

    /// Lock already contains something.
    const ELockIsFull: u64 = 2;

    /// Lock that stores any content inside it.
    public struct Lock<T: store + key> has key {
        id: UID,
        locked: Option<T>
    }

    /// A key that is created with a Lock; is transferable
    /// and contains all the needed information to open the Lock.
    public struct Key<phantom T: store + key> has key, store {
        id: UID,
        lock_id: ID,
    }

    /// Returns an ID of a Lock for a given Key.
    public fun key_for<T: store + key>(key: &Key<T>): ID {
        key.lock_id
    }

    /// 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.
    ///
    /// Return the Key to the caller so they decide what to do with it.
    public fun create<T: store + key>(obj: T, ctx: &mut TxContext): Key<T> {
        let id = object::new(ctx);
        let lock_id = id.to_inner();

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

        Key<T> { id: object::new(ctx), lock_id }
    }

    /// Lock something inside a shared object using a Key. Aborts if
    /// lock is not empty or if key doesn't match the lock.
    public fun lock<T: store + key>(
        obj: T,
        lock: &mut Lock<T>,
        key: &Key<T>,
    ) {
        assert!(lock.locked.is_none(), ELockIsFull);
        assert!(&key.lock_id == object::borrow_id(lock), EKeyMismatch);

        lock.locked.fill(obj);
    }

    /// 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!(lock.locked.is_some(), ELockIsEmpty);
        assert!(&key.lock_id == object::borrow_id(lock), EKeyMismatch);

        lock.locked.extract()
    }
}

This pattern is used in these examples:

Testing

Tests are an important part of any software project. They are especially important in blockchain applications because smart contracts control digital assets. Move has a built-in testing framework that allows you to write tests for your modules.

To test a package, use the sui move test command. This command will run all the tests in the package, and print the results to the console.

$ sui move test

Simple Test

Tests are functions, marked with the #[test] attribute. They are not included into the compiled program, and are only used for testing purposes.

module examples::simple_tests {

    /// A point in 2D space. Has an `x` and `y` coordinate.
    public struct Point has copy, drop {
        x: u8,
        y: u8,
    }

    /// Create a point.
    public fun new(x: u8, y: u8): Point { Point { x, y } }

    /// Move a point to a new location.
    public fun move_xy(p: &mut Point, x: u8, y: u8) {
        p.x = x;
        p.y = y;
    }

    /// Get the x coordinate of a point.
    public fun x(p: &Point): u8 { p.x }

    /// Get the y coordinate of a point.
    public fun y(p: &Point): u8 { p.y }

    #[test]
    // The function marked with `#[test]` is a test. The name of the function
    // will be shown in the test output, so it should be descriptive.
    //
    // The function doesn't take any arguments, and doesn't return anything.
    fun test_point_new_and_move() {
        let mut p1 = new(1, 2);

        // normally, a test should contain correctness assertions
        assert!(x(&p1) == 1, 0);
        assert!(y(&p1) == 2, 1);

        move_xy(&mut p1, 3, 4);

        // while not a requirement, abort codes should be unique,
        // it's easier to debug when they are
        assert!(x(&p1) == 3, 2);
        assert!(y(&p1) == 4, 3);
    }
}

Test Fail Cases

Tests should also cover the failure cases. Move provides a special attribute to mark tests that are expected to fail. The #[expected_failure] attribute can be used to mark a test that is expected to fail. It can be used with or without an abort code. If an abort code is provided, the test will fail if it does not abort with the provided code. If no abort code is provided, the test will fail if it does not abort.

module examples::expected_failure {
    /// Trying to take a value out of an empty container.
    const EContainerEmpty: u64 = 0;
    /// Trying to put a value into a full container.
    const EContainerFull: u64 = 1;
    /// Function is not implemented.
    const ENotImplemented: u64 = 3;

    /// A container that can hold a value of type T or nothing.
    public struct Container<T> has copy, store, drop {
        inner: Option<T>
    }

    /// Creates a new container with the given value.
    public fun new<T>(inner: T): Container<T> {
        Container { inner: option::some(inner) }
    }

    /// Creates a new empty container.
    public fun empty<T>(): Container<T> {
        Container { inner: option::none() }
    }

    /// Takes the value out of the container. Aborts if the container is empty.
    public fun take<T>(container: &mut Container<T>): T {
        assert!(container.inner.is_some(), EContainerEmpty);
        container.inner.extract()
    }

    /// Put a value into the container. Aborts if the container is full.
    public fun put<T>(container: &mut Container<T>, value: T) {
        assert!(container.inner.is_none(), EContainerFull);
        container.inner.fill(value);
    }

    #[test]
    // An example of a regular test function. Not expected to fail.
    fun create_empty() {
        let container = empty<u64>();
        assert!(container.inner.is_none(), 0);
    }

    #[test]
    #[expected_failure]
    // The "expected_failure" attribute can be added to a test function. And
    // the test will pass if it the function fails with any abort code.
    fun showcase_expected_failure_fail() {
        abort ENotImplemented
    }

    #[test]
    #[expected_failure(abort_code = EContainerEmpty)]
    // It is considered a good practice to specify the expected abort code. It
    // is especially important when a function has multiple abort points.
    //
    // The abort code is a constant that is used to identify the expected
    // code. It can be both a value and a name of a constant. It also allows
    // importing constants from other modules!
    fun try_take_from_empty_container_fail() {
        let mut container = empty<u64>();
        let _ = container.take();
    }

    #[test, expected_failure(abort_code = EContainerFull)]
    // Attributes can be combined into a single attribute list, separated by
    // commas. Purely a style choice.
    fun try_put_into_full_container_fail() {
        let mut container = new(42u64); // create a container with a value
        container.put(42); // try to put another value into it
    }
}

Dummy Context

This example illustrates how to create a TxContext for object testing without using the Test Scenario framework.

/// This module contains an example of a simple application with a simple set of
/// rules: a letter can only be read once, and then it is burned.
///
/// The tests in this module demonstrate how to use the `tx_context` module to
/// create a custom `TxContext` for testing purposes without using a heavier
/// `test_scenario` module.
///
/// The UIDs of objects generated with dummy context will remain the same in
/// every test, so the consistency of the tests is guaranteed. However, for a
/// single test, there should be only one `tx_context::dummy()` call, otherwise
/// it is possible to create objects with the same UID in the same test.
module examples::burn_after_reading {
    use std::string::String;

    /// Attempt to burn before reading.
    const ENotReadYet: u64 = 0;

    /// A simple letter Object which can be read and then burned.
    public struct Letter has key, store {
        id: UID,
        content: String,
        is_read: bool,
        sender: address,
    }

    /// Write a new letter, signed by the sender.
    public fun new(content: String, ctx: &mut TxContext): Letter {
        Letter {
            id: object::new(ctx),
            content,
            is_read: false,
            sender: ctx.sender()
        }
    }

    /// Read the letter and get the contents.
    public fun read(l: &mut Letter): String {
        l.is_read = true;
        l.content
    }

    /// Burn once read. Only sender address can be recovered from the ashes, the
    /// letter is gone forever.
    public fun burn(l: Letter): address {
        let Letter { id, content: _, is_read, sender } = l;
        assert!(is_read, ENotReadYet);
        id.delete();
        sender
    }

    #[test]
    // This test uses the dummy context to create and read a letter. By using
    // the `tx_context::dummy()` function, we can create a dummy context which
    // has the `sender = 0x0` and `epoch = 0`. This allows us to create objects
    // without using a more complex test scenario.
    fun test_new_read_letter() {

        // This is where the magic happens! Because context has `drop`, we can
        // assign it directly as `&mut` reference and use in the test.
        let ctx = &mut tx_context::dummy();
        let mut letter = new(b"Directed by Ethan and Joel Coen".to_string(), ctx);

        // burn after reading
        read(&mut letter);
        burn(letter);
    }

    #[test]
    // It is also possible to create custom contexts for testing. Especially if
    // there's a need to test the behavior of the contract based on the sender
    // or the epoch.
    //
    // The `tx_context::new_from_hint` function can create a context with tx
    // hash generated from a number. Alternatively, you can use `tx_context::new`
    // and provide 32-byte hash manually.
    fun test_fan_letter() {

        // this is the best way to create a custom `TxContext`
        let ctx = &mut tx_context::new_from_hint(
            @0xC4AD,  // sender
            0u64,     // hint, used to generate tx hash
            1,        // epoch
            0,        // epoch_timestamp_ms
            0,        // `ids_created` (normally should be `0`)
        );

        let mut letter = new(b"Uhhh... Osbourne?... Osbourne Cox?".to_string(), ctx);

        // read, burn, but keep the sender
        read(&mut letter);
        let sender = burn(letter);

        // check the sender is correct
        assert!(sender == @0xC4AD, 0);
    }

    #[test, expected_failure(abort_code = ENotReadYet)]
    // This test makes sure the letter cannot be burned before reading. And
    // expects the transaction to fail with `ENotReadYet` abort code.
    //
    // It uses the `tx_context::new` method with a custom tx hash.
    fun test_burn_before_read() {
        use sui::hash::blake2b256;

        let tx_hash = blake2b256(&b"some seed for hashing");
        let ctx = &mut tx_context::new(
            @0x0,    // sender
            tx_hash, // tx hash
            0,       // epoch
            0,       // epoch_timestamp_ms
            0,       // `ids_created` (normally should be `0`)
        );

        let letter = new(b"What did we learn, Palmer?".to_string(), ctx);
        burn(letter);
    }
}

Test Scenario

For complicated transaction testing, Sui features the test_scenario module. This module provides functions to emulate transactions, define senders, and check the results of transactions.

/// This module contains a dummy store implementation where anyone can purchase
/// the same book for any amount of SUI greater than zero. The store owner can
/// collect the proceeds using the `StoreOwnerCap` capability.
///
/// In the tests section, we use the `test_scenario` module to simulate a few
/// transactions and test the store functionality. The test scenario is a very
/// powerful tool which can be used to simulate multiple transactions in a single
/// test.
///
/// The reference for this module is the "Black Books" TV series.
module examples::black_books {
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};

    /// Trying to purchase the book for 0 SUI.
    const ECantBeZero: u64 = 0;

    /// A store owner capability. Allows the owner to collect proceeds.
    public struct StoreOwnerCap has key, store { id: UID }

    /// The "Black Books" store located in London.
    /// Only sells one book: "The Little Book of Calm".
    public struct BlackBooks has key {
        id: UID,
        balance: Balance<SUI>,
    }

    /// The only book sold by the Black Books store.
    public struct LittleBookOfCalm has key, store { id: UID }

    /// Share the store object and transfer the store owner capability to the sender.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(StoreOwnerCap {
            id: object::new(ctx)
        }, ctx.sender());

        transfer::share_object(BlackBooks {
            id: object::new(ctx),
            balance: balance::zero()
        })
    }

    /// Purchase the "Little Book of Calm" for any amount of SUI greater than zero.
    public fun purchase(
        store: &mut BlackBooks, coin: Coin<SUI>, ctx: &mut TxContext
    ): LittleBookOfCalm {
        assert!(coin.value() > 0, ECantBeZero);
        store.balance.join(coin.into_balance());

        // create a new book
        LittleBookOfCalm { id: object::new(ctx) }
    }

    /// Collect the proceeds from the store and return them to the sender.
    public fun collect(
        store: &mut BlackBooks, _cap: &StoreOwnerCap, ctx: &mut TxContext
    ): Coin<SUI> {
        let amount = store.balance.value();
        store.balance.split(amount).into_coin(ctx)
    }

    // === Tests ===

    #[test_only]
    // The `init` is not run in tests, and normally a test_only function is
    // provided so that the module can be initialized in tests. Having it public
    // is important for tests located in other modules.
    public fun init_for_testing(ctx: &mut TxContext) {
        init(ctx);
    }

    // using a test-only attibute because this dependency can't be used in
    // production code and `sui move build` will complain about unused imports.
    //
    // the `sui::test_scenario` module is only available in tests.
    #[test_only] use sui::test_scenario;

    #[test]
    // This test uses `test_scenario` to emulate actions performed by 3 accounts.
    // A single scenario follows this structure:
    //
    // - `begin` - starts the first tx and creates the sceanario
    // - `next_tx` ... - starts the next tx and sets the sender
    // - `end` - wraps up the scenario
    //
    // It provides functions to start transactions, get the `TxContext, pull
    // objects from account inventory and shared pool, and check transaction
    // effects.
    //
    // In this test scenario:
    // 1. Bernard opens the store;
    // 2. Manny buys the book for 10 SUI and sends it to Fran;
    // 3. Fran sends the book back and buys it herself for 5 SUI;
    // 4. Bernard collects the proceeds and transfers the store to Fran;
    fun the_book_store_drama() {
        // it's a good idea to name addresses for readability
        // Bernard is the store owner, Manny is searching for the book,
        // and Fran is the next door store owner.
        let (bernard, manny, fran) = (@0x1, @0x2, @0x3);

        // create a test scenario with sender; initiates the first transaction
        let mut scenario = test_scenario::begin(bernard);

        // === First transaction ===

        // run the module initializer
        // we use curly braces to explicitly scope the transaction;
        {
            // `test_scenario::ctx` returns the `TxContext`
            init_for_testing(scenario.ctx());
        };

        // `next_tx` is used to initiate a new transaction in the scenario and
        // set the sender to the specified address. It returns `TransactionEffects`
        // which can be used to check object changes and events.
        let prev_effects = scenario.next_tx(manny);

        // make assertions on the effects of the first transaction
        let created_ids = prev_effects.created();
        let shared_ids = prev_effects.shared();
        let sent_ids = prev_effects.transferred_to_account();
        let events_num = prev_effects.num_user_events();

        assert!(created_ids.length() == 2, 0);
        assert!(shared_ids.length() == 1, 1);
        assert!(sent_ids.size() == 1, 2);
        assert!(events_num == 0, 3);

        // === Second transaction ===

        // we will store the `book_id` in a variable so we can use it later
        let book_id = {
            // test scenario can pull shared and sender-owned objects
            // here we pull the store from the pool
            let mut store = scenario.take_shared<BlackBooks>();
            let ctx = scenario.ctx();
            let coin = coin::mint_for_testing<SUI>(10_000_000_000, ctx);

            // call the purchase function
            let book = store.purchase(coin, ctx);
            let book_id = object::id(&book);

            // send the book to Fran
            transfer::transfer(book, fran);

            // now return the store to the pool
            test_scenario::return_shared(store);

            // return the book ID so we can use it across transactions
            book_id
        };

        // === Third transaction ===

        // next transaction - Fran looks in her inventory and finds the book
        // she decides to return it to Manny and buy another one herself
        scenario.next_tx(fran);
        {
            // objects can be taken from the sender by ID (if there's multiple)
            // or if there's only one object: `take_from_sender<T>(&scenario)`
            let book = scenario.take_from_sender_by_id<LittleBookOfCalm>(book_id);
            // send the book back to Manny
            transfer::transfer(book, manny);

            // now repeat the same steps as before
            let mut store = scenario.take_shared<BlackBooks>();
            let ctx = scenario.ctx();
            let coin = coin::mint_for_testing<SUI>(5_000_000_000, ctx);

            // same as before - purchase the book
            let book = store.purchase(coin, ctx);
            transfer::transfer(book, fran);

            // don't forget to return
            test_scenario::return_shared(store);
        };

        // === Fourth transaction ===

        // last transaction - Bernard collects the proceeds and transfers the store to Fran
        test_scenario::next_tx(&mut scenario, bernard);
        {
            let mut store = scenario.take_shared<BlackBooks>();
            let cap = scenario.take_from_sender<StoreOwnerCap>();
            let ctx = scenario.ctx();
            let coin = store.collect(&cap, ctx);

            transfer::public_transfer(coin, bernard);
            transfer::transfer(cap, fran);
            test_scenario::return_shared(store);
        };

        // finally, the test scenario needs to be finalized
        scenario.end();
    }
}

Test Only

The #[test_only] attribute allows creating a module member that will only be available in the test environment. This is useful for creating initializers and other helper functions that are needed for testing but would be dangerous to expose in the production environment. The attribute can also be used to define test-only types, friends and dependencies.

module examples::registry {
    use std::type_name::{Self, TypeName};

    /// Singleton object that can be created only once.
    public struct TypeRegistry has key {
        id: UID,
        /// Simple counter to keep track of the number of records.
        records: vector<TypeName>,
    }

    /// Create and share a new `Registry` object.
    fun init(ctx: &mut TxContext) {
        sui::transfer::share_object(TypeRegistry {
            id: object::new(ctx),
            records: vector[]
        })
    }

    /// Add a new record to the `Registry` object with the type name of `T`.
    public fun register_type<T>(r: &mut TypeRegistry) {
        r.records.push_back(type_name::get<T>())
    }

    /// Getter for the `records` field.
    public fun records(registry: &TypeRegistry): &vector<TypeName> {
        &registry.records
    }

    // === Test and test-only members ===

    #[test_only]
    // The #[test_only] attribute makes the function only available in test mode.
    // Meaning that it can be used in functions marked with #[test] and in other
    // functions marked with #[test_only].
    //
    // We allow creating a TypeRegistry object for tests! Also, in tests it's often
    // a good idea to allow for customizing the object's state.
    public fun new_for_testing(ctx: &mut TxContext): TypeRegistry {
        TypeRegistry {
            id: object::new(ctx),
            records: vector[]
        }
    }

    #[test_only]
    // It's also a good idea to mark test_only functions with suffixes like
    // `_for_testing` to make it clear that they are only available in tests.
    public fun reset_for_testing(reg: &mut TypeRegistry) {
        reg.records = vector[];
    }


    // Test_only can be applied to any module member. Including structs!
    #[test_only] public struct TestOnlyStruct {}

    // Dependencies can be marked as "test_only".
    // Some dependencies are only available in test mode such as `sui::test_utils`.
    #[test_only] use sui::test_utils;

    #[test]
    // In tests we can use the `new_for_testing` function to create a new
    // `Registry` object.
    fun test_registry() {
        let ctx = &mut tx_context::dummy();
        let mut registry = new_for_testing(ctx);

        assert!(registry.records().length() == 0, 0);

        // we can use `TestOnlyStruct` because it's marked with `#[test_only]`
        register_type<TestOnlyStruct>(&mut registry);

        assert!(registry.records().length() == 1, 0);

        // super helpful utility which is only available in tests!
        test_utils::destroy(registry);
    }
}

#[test_only]
// #[test_only] can also be applied to modules. All of the module's members
// will be test_only and don't need to be marked individually.
module examples::registry_tests {
    use sui::test_utils;
    use examples::registry::{Self, TestOnlyStruct};

    public struct AnotherTestOnlyStruct {}

    #[test]
    fun test_with_two_structs() {
        let ctx = &mut tx_context::dummy();
        let mut reg = registry::new_for_testing(ctx);

        reg.register_type<TestOnlyStruct>();
        reg.reset_for_testing();

        assert!(reg.records().length() == 0, 0);

        reg.register_type<AnotherTestOnlyStruct>();

        assert!(reg.records().length() == 1, 0);

        test_utils::destroy(reg);
    }
}

Samples

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

NFT

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

module examples::puppy {
    use std::string::String;
    use sui::event;

    /// An example NFT that can be minted by anybody. A Puppy is
    /// a freely-transferable object. Owner can add new traits to
    /// their puppy at any time and even change the image to the
    /// puppy's liking.
    public struct Puppy has key, store {
        id: UID,
        /// Name of the Puppy
        name: String,
        /// Grumpy or chill?
        traits: vector<String>,
        /// The URL of the Puppy's image
        url: String,
    }

    /// Event: emitted when a new Puppy is minted.
    public struct PuppyMinted has copy, drop {
        /// ID of the Puppy
        puppy_id: ID,
        /// The address of the NFT minter
        minted_by: address,
    }

    /// Mint a new Puppy with the given `name`, `traits` and `url`.
    /// The object is returned to sender and they're free to transfer
    /// it to themselves or anyone else.
    public fun mint(
        name: String,
        traits: vector<String>,
        url: String,
        ctx: &mut TxContext
    ): Puppy {
        let id = object::new(ctx);

        event::emit(PuppyMinted {
            puppy_id: id.to_inner(),
            minted_by: ctx.sender(),
        });

        Puppy { id, name, traits, url }
    }

    /// Some puppies get new traits over time... owner of one can
    /// add a new trait to their puppy at any time.
    public fun add_trait(puppy: &mut Puppy, trait: String) {
        puppy.traits.push_back(trait);
    }

    /// As the puppy grows, owners can change the image to reflect
    /// the puppy's current state and look.
    public fun set_url(puppy: &mut Puppy, url: String) {
        puppy.url = url;
    }

    /// It's a good practice to allow the owner to destroy the NFT
    /// and get a storage rebate. Not a requirement and depends on
    /// your use case. At last, who doesn't love puppies?
    public fun destroy(puppy: Puppy) {
        let Puppy { id, url: _, name: _, traits: _ } = puppy;
        id.delete()
    }

    // Getters for properties.
    // Struct fields are always private and unless there's a getter,
    // other modules can't access them. It's up to the module author
    // to decide which fields to expose and which to keep private.

    /// Get the Puppy's `name`
    public fun name(puppy: &Puppy): String { puppy.name }

    /// Get the Puppy's `traits`
    public fun traits(puppy: &Puppy): &vector<String> { &puppy.traits }

    /// Get the Puppy's `url`
    public fun url(puppy: &Puppy): String { puppy.url }
}

Create a Coin

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 sui::coin;

    /// 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.
    public struct MYCOIN has drop {}

    /// 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,                // decimals
            b"MYC",           // symbol
            b"My Coin",       // name
            b"Don't ask why", // description
            option::none(),   // icon url
            ctx
        );

        // transfer the `TreasuryCap` to the sender, so they can mint and burn
        transfer::public_transfer(treasury, ctx.sender());

        // metadata is typically frozen after creation
        transfer::public_freeze_object(metadata);
    }
}

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.

Regulated Coin

Sui provides a way to create a Coin with a global Denylist. This is useful for creating a regulated coin, where the issuer can restrict certain accounts from holding the coin. The denylist functionality is native to the network and maintains the fast path.

module examples::reg_coin {
    use sui::coin;

    /// The type identifier of coin. The coin will have a type
    /// tag of kind: `Coin<package_object::reg_coin::REG_COIN>`
    /// Make sure that the name of the type matches the module's name.
    public struct REG_COIN has drop {}

    /// 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: REG_COIN, ctx: &mut TxContext) {
        // Unlike regular currency, regulated currency uses a different method.
        let (treasury, denycap, metadata) = coin::create_regulated_currency(
            witness,
            6,                           // decimals
            b"REG",                      // symbol
            b"My Coin",                  // name
            b"Do good, and you're good", // description
            option::none(),              // icon url
            ctx
        );

        // transfer the `TreasuryCap` to the sender, so they can mint and burn
        transfer::public_transfer(treasury, ctx.sender());

        // transfer the `DenyCap` to the sender, so they can freeze and
        // unfreeze accounts from using the currency
        transfer::public_transfer(denycap, ctx.sender());

        // metadata is typically frozen after creation
        transfer::public_freeze_object(metadata);
    }

    // === Entry Functions (can be replaced by direct calls in PTBs) ===

    use sui::deny_list::DenyList;
    use sui::coin::DenyCap;

    /// Add an account to denylist preventing them from using the currency.
    /// DenyList is a special shared object with reserved address `0x403`.
    entry fun block_account(
        deny_list: &mut DenyList,
        deny_cap: &mut DenyCap<REG_COIN>,
        account: address,
        ctx: &mut TxContext
    ) {
        coin::deny_list_add(deny_list, deny_cap, account, ctx);
    }

    /// Remove an account from denylist allowing them to use the currency.
    /// DenyList is a special shared object with reserved address `0x403`.
    entry fun unblock_account(
        deny_list: &mut DenyList,
        deny_cap: &mut DenyCap<REG_COIN>,
        account: address,
        ctx: &mut TxContext
    ) {
        coin::deny_list_remove(deny_list, deny_cap, account, ctx);
    }
}

Currently, the Denylist is only supported for the Coin type. In the future, it may be extended to some other system types.

Additional Resources

To find out more about Sui Move and its usage, check out these links: