見出し画像

【Sui開発】基本的なNFT発行のコントラクトの書き方(Move2024対応)

Sui Moveを勉強中のおにぎりです💧

Moveの勉強を始めた当初、一番印象的だったのはSuiの公式ドキュメントに記載されているとある文章でした

Suiでは、すべてがオブジェクトである。さらにSuiでは、そのオブジェクトはユニークで、腐敗せず、所有することができるため、すべてが腐敗しないトークン(NFT)である。つまり、技術的には、特定のNFTを作成するには、基本的なタイプパブリッシングだけで十分なのだ。

https://docs.sui.io/guides/developer/sui-101/create-nft

Suiではすべてがオブジェクトで、そのオブジェクトにはグローバルで一意のIDが振られるので、そういう意味ではすべてがNFTと言えるってことですね

そのため、NFTを作るにはSuiの基本的な機能だけで十分、と言っています

これを実感するために、NFT発行のコントラクトを書いてみました。今回はそのコードの紹介をしたいと思います💻

NFT発行コントラクト(モジュール)のコード

以下がSui Moveで書いたコードの全体です。115行のコードです

主にSuiの公式ガイド「Create a Non-Fungible Token」を参考にしています

次の章から重要な部分についての説明をします

module onigiri::mizu_nft {
    // === Imports ===
    use std::string::{utf8};
    use sui::event;
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};
    use sui::sui::{SUI};
    use sui::display::{Self};
    use sui::package::{Self};

    // === Constants ===
    const EPaymentError: u64 = 0;
    const ENFTLimitError: u64 = 1;

    // === Structs ===
    public struct MIZU_NFT has drop {}

    public struct AdminCap has key {
        id: UID,
    }

    public struct MizuNFT has key, store {
        id: UID,
        number: u64,
    }

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

    public struct NFTMinted has copy, drop {
        object_id: ID,
        minted_by: address,
    }

    // === Init Function ===
    fun init(otw: MIZU_NFT, ctx: &mut TxContext) {
        transfer::transfer(AdminCap {
            id: object::new(ctx),
        }, ctx.sender());

        let keys = vector[
            utf8(b"name"),
            utf8(b"image_url")
        ];

        let values = vector[
            utf8(b"Mizu NFT #{number}"),
            utf8(b"https://placehold.jp/33ddff/ffffff/150x150.png")
        ];

        let publisher = package::claim(otw, ctx);

        let mut display = display::new_with_fields<MizuNFT>(&publisher, keys, values, ctx);
        display::update_version(&mut display);

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

        transfer::share_object(NFTShop {
            id: object::new(ctx),
            balance: balance::zero(),
            count: 0,
            limit: 3,
            price: 100_000_000, // 0.1 SUI
        });
    }

    entry fun pay_mint(payment: Coin<SUI>, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        assert!(payment.value() == nft_shop.price, EPaymentError);
        nft_shop.balance.join(payment.into_balance());

        let nft = mint_(nft_shop, ctx);
        transfer::public_transfer(nft, ctx.sender());
    }

    fun mint_(nft_shop: &mut NFTShop, ctx: &mut TxContext): MizuNFT {
        assert!(nft_shop.count + 1 <= nft_shop.limit, ENFTLimitError);

        let nft = MizuNFT {
            id: object::new(ctx),
            number: nft_shop.count,
        };

        nft_shop.count = nft_shop.count + 1;

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

        nft
    }

    public fun burn(nft: MizuNFT, _: &mut TxContext) {
        let MizuNFT { id, number: _ } = nft;
        id.delete();
    }

    // === Admin Functions ===
    entry fun admin_mint(_: &AdminCap, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        let nft = mint_(nft_shop, ctx);
        transfer::public_transfer(nft, ctx.sender());
    }

    entry fun withdraw(_: &AdminCap, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        let balance = nft_shop.balance.withdraw_all();
        transfer::public_transfer(coin::from_balance(balance, ctx), ctx.sender());
    }
}

コントラクトで行っていること

コントラクト内のコードでは以下のことを行なっています。

  • packageのインポート

  • const変数の定義

  • struct(構造体)の定義

  • init(一度だけ呼ばれる)関数の定義

  • mint(NFT発行)関数の定義

  • burn(NFT焼却)関数の定義

  • withdraw(売上の引き出し)関数の定義

核となるstruct, init, mint, burn, withdrawについての解説をしていきます。

Struct

    // === Structs ===
    public struct MIZU_NFT has drop {}

    public struct AdminCap has key {
        id: UID,
    }

    public struct MizuNFT has key, store {
        id: UID,
        number: u64,
    }

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

    public struct NFTMinted has copy, drop {
        object_id: ID,
        minted_by: address,
    }

Structの定義においてNFTの核となっているstructは、 MizuNFTとNFTShop、そしてAdminCapです

MizuNFT
これがNFTそのものになります。keyとstore能力によってNFTになります。とてもシンプルですね!今回はidとnumberだけですが、それ以外によくあるようなattributes(属性)を追加してもいいですね

これはミント時に、ミントしたアドレスが所有するオブジェクトになります

NFTShop
これはkey能力のみを持っているオブジェクトです。NFT販売店のような意味合いで、NFTの価格、発行上限、現在ミントされている数、そして売上を管理するオブジェクトです

これは後術する一度しか呼ばれない関数であるinit関数において作成される変更可能な共有オブジェクトです

AdminCap
管理者権限用のオブジェクトです。このオブジェクトの所有者のみが実行できる関数を作成したいときに、このオブジェクトを引数に設定します。
こちらもinit関数で作成されます。コントラクト作成者(sender)に送信される所有オブジェクトです

💡 MizuNFTで keyとstore能力によってNFTになる、と記載しましたが、厳密にはkeyだけでも問題ありません。ただしその場合、moduleでtransferメソッドを実装しないとNFTを送ることができなくなってしまいます。

init関数

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

        let keys = vector[
            utf8(b"name"),
            utf8(b"image_url")
        ];

        let values = vector[
            utf8(b"Mizu NFT #{number}"),
            utf8(b"https://placehold.jp/33ddff/ffffff/150x150.png")
        ];

        let publisher = package::claim(otw, ctx);

        let mut display = display::new_with_fields<MizuNFT>(&publisher, keys, values, ctx);
        display::update_version(&mut display);

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

        transfer::share_object(NFTShop {
            id: object::new(ctx),
            balance: balance::zero(),
            count: 0,
            limit: 3,
            price: 100_000_000, // 0.1 SUI
        });
    }

init関数は、モジュールを公開するときに一度だけ呼ばれる特別な関数です。一度だけしか呼ばれないということを活かして、管理者用オブジェクトの作成などを行ったりします

この関数では、AdminCapを作成してモジュール作成者に送信したり、NFTのメタデータをフロントエンド(Webサイト)に表示させるためにテンプレートエンジンを使っていたり、NFTShopという共有オブジェクトを作成したりしています

mint関数

    entry fun pay_mint(payment: Coin<SUI>, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        // ①
        assert!(payment.value() == nft_shop.price, EPaymentError);
        // ②
        nft_shop.balance.join(payment.into_balance());

        let nft = mint_(nft_shop, ctx);
        transfer::public_transfer(nft, ctx.sender());
    }

    fun mint_(nft_shop: &mut NFTShop, ctx: &mut TxContext): MizuNFT {
        // ①
        assert!(nft_shop.count + 1 <= nft_shop.limit, ENFTLimitError);
                // ②
        let nft = MizuNFT {
            id: object::new(ctx),
            number: nft_shop.count,
        };
                // ③
        nft_shop.count = nft_shop.count + 1;

        event::emit(NFTMinted {
            object_id: object::id(&nft),
            minted_by: ctx.sender(),
        });
                // ④
        nft
    }

    // === Admin Functions ===
    entry fun admin_mint(_: &AdminCap, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        let nft = mint_(nft_shop, ctx);
        transfer::public_transfer(nft, ctx.sender());
    }

NFTを発行するmint関数については、3つの関数を作成しています

mint_
module内でしか呼ぶことのできないプライベートな関数です。pay_mintとadmin_mint関数の内部で呼んでいます。

mint_関数で行っていることは以下の通りです

①まずNFT発行上限を超えていないかを確認します
②問題なければMizuNFTをインスタンス化し、
③NFTShopのcountを増やし、
④MizuNFTインスタンスを戻り値として返します

pay_mint
有料のmint関数です

①渡されたSUIオブジェクトが、Mint価格(0.1 SUI)と同じ値かを確認します
②問題なければNFTShopの残高(balance)に統合します

そしてmint_関数を呼び出しNFTを作成してから、トランザクション実行者に送信をします

admin_mint
管理者が実行できる無料のmint関数です。AdminCapを引数に取ることで、AdminCapを持っている管理者にしか実行できない関数にすることができます。やっていることはpay_mintの最後の2行と同等です

burn関数

    public fun burn(nft: MizuNFT, _: &mut TxContext) {
        let MizuNFT { id, number: _ } = nft;
        id.delete();
    }

NFTの焼却(削除)の関数です。やっていることはとてもシンプルで、MizuNFTのObject IDを取り出し、そのIDを使ってobjectのdelete関数を呼んで削除しています

オブジェクトを削除すると、SuiVisionでは "This object was deleted. You can view its history here." と表示されます

NFTは見えなくなりますが、NFTというオブジェクトに対して行われた過去のトランザクションは消えません

withdraw関数

    entry fun withdraw(_: &AdminCap, nft_shop: &mut NFTShop, ctx: &mut TxContext) {
        let balance = nft_shop.balance.withdraw_all();
        transfer::public_transfer(coin::from_balance(balance, ctx), ctx.sender());
    }

管理者しか実行できない引き出しの関数です。balanceのwithdraw_all関数で残高を取得し、トランザクション実行者に送信しています

まとめ

いかがだったでしょうか。今回のNFT発行コントラクトの解説を通じて、Sui Moveの基本的な仕組みや特性について理解を深めていただけたのではないかと思います

今回のコントラクトは基本的な機能しかありませんが、例えばこれに利益分配の仕組みをいれてみたり、NFTとNFTを融合させるといった仕掛けを考えて、独自のコントラクトを書いてみると楽しそうですね!

それでは Happy Building!!


この記事が気に入ったらサポートをしてみませんか?