🎃

Aptosを使ってたまごっちライクなブロックチェーンゲームを作ってみよう!! 〜Part 2〜

2024/03/16に公開

はじめに

皆さん、こんにちは!

今回はパブリックブロックチェーンの一つであるAptosをテーマにした記事になります!

2024年現在 ハッカソンプラットフォームAkindoとAptosのチームがタッグ組んでWaveHackというグラントプログラムを実施中です!

グラントには2つの部門があるのですが、今回はそのうちの一つである

Create Aptos Move Contents

部門への応募も兼ねた記事になっています!!

対象となっているAptosのドキュメントを翻訳するだけでグラントを取得できる可能性があり、非エンジニアでもグラントを獲得できる非常に貴重な機会となっていますので皆さんもぜひ挑戦してみてくださいね!!

詳細は下記サイトにて紹介されています!

https://app.akindo.io/wave-hacks/Nmqgo639ai9ZQMB1

https://app.akindo.io/wave-hacks/Z47L4rwQmI774vRpr

https://lu.ma/aptos-wavehack

Move言語について

Move言語については White Yuseiさんの資料の解説が素晴らしいので共有いたします!!

https://www.canva.com/design/DAF_tifX6Uw/q9VL8oMJM4f2MY-hzeAa_A/view

今回翻訳に挑戦するドキュメント

Aptosチームが出しているラーニングサイトの以下の記事の翻訳に挑戦します!!

https://learn.aptoslabs.com/example/aptogotchi-intermediate

Aptos上でたまごっちライクなブロックチェーンゲームが構築できるようです!!

https://aptogotchi.aptoslabs.com/

Part1の記事が見てみたいという方はぜひ下記もチェックしてみてください!!

https://zenn.dev/mashharuki/articles/7d744afc2f4d73

今回お手本となるGitHubのリポジトリは下記です!

https://github.com/aptos-labs/aptogotchi-intermediate

私は前回のリポジトリをベースにブランチを切って更新しています!!

https://github.com/mashharuki/aptogotchi/tree/aptogotchi-intermediate

この学習コンテンツでは次のことが学べます!!

ではスタートです!!

Aptosにおけるデジタル・アセットとは

以下のページを対象にしています。

https://learn.aptoslabs.com/example/aptogotchi-intermediate/digital-assets

単純なNFTというわけではなくさらに利活用しやすいように拡張性も持たせることができるみたいですね。

  • なぜNFTではなく、デジタルアセットなのか?
    デジタルアセットでは、Moveで伝統的に使用されてきたアカウントリソースではなく、Aptosオブジェクトを使用します。これにより、アカウントの外部にデータを保存することができ、柔軟性が増します。

https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/object.move

トークンは、フレームワークを変更することなく、カスタムデータや機能で簡単に拡張できます。

次のような機能を持たせることができるようです。

  • 転送は単に参照更新である。
  • 直接譲渡はオプトインなしで可能です。
  • NFTは他のNFTを所有することができ、コンポーザビリティが容易になります。
  • ソウルバインドトークンも簡単にサポートできます。

デジタルアセットがAptosにおけるトークンの新基準であり、その使用が推奨されているようです。

Aptogochiにおけるデジタルアセットの実装

Aptogochiで発行されるNFTにもこの規格が採用されています。

コードベースだと下記部分が該当する実装部分ですね

// ==== ACCESSORIES ====
// Create an Aptogotchi token object
public entry fun create_accessory(user: &signer, category: String) acquires CollectionCapability {
    let uri = string::utf8(ACCESSORY_COLLECTION_URI);
    let description = string::utf8(ACCESSORY_COLLECTION_DESCRIPTION);

    let constructor_ref = token::create_named_token(
        &get_token_signer(),
        string::utf8(ACCESSORY_COLLECTION_NAME),
        description,
        get_accessory_token_name(&address_of(user), category),
        option::none(),
        uri,
    );

    let token_signer = object::generate_signer(&constructor_ref);
    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    let category = string::utf8(ACCESSORY_CATEGORY_BOWTIE);
    let id = 1;

    let accessory = Accessory {
        category,
        id,
    };

    move_to(&token_signer, accessory);
    object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), address_of(user));
}

この考え方は、ERC6551に似てますね・・。Aptosではそれが標準になっているみたいです。

ERC6551について知りたい方は下記ブログがおすすめです!!

https://zenn.dev/thirdweb_jp/articles/ce2c7bdc39ff85

https://recruit.gmo.jp/engineer/jisedai/blog/erc-6551/

ではAptosにおけるデジタルアセットを転送するときの考え方も学んでいきましょう!

この時点で aptogochi.moveファイルは以下のように実装することになります。

前回のコードと比較してAtogochiに色々アクセサリー(デジタルアセット)を追加する機能が追加されています。

food モジュールについては次に解説するので今はビルドでエラーが出てもOKです。

module aptogotchi::main {
    use aptogotchi::food;
    use aptos_framework::account::{Self, SignerCapability};
    use aptos_framework::aptos_coin::AptosCoin;
    use aptos_framework::coin;
    use aptos_framework::event;
    use aptos_framework::object;
    use aptos_framework::timestamp;
    use aptos_std::string_utils::{to_string};
    use aptos_token_objects::collection;
    use aptos_token_objects::token;
    use std::error;
    use std::option;
    use std::signer::address_of;
    use std::signer;
    use std::string::{Self, String};
    use std::vector;

    /// aptogotchi not available
    const ENOT_AVAILABLE: u64 = 1;
    /// accessory not available
    const EACCESSORY_NOT_AVAILABLE: u64 = 1;
    const EPARTS_LIMIT: u64 = 2;
    const ENAME_LIMIT: u64 = 3;
    const EUSER_ALREADY_HAS_APTOGOTCHI: u64 = 4;
    // maximum health points: 5 hearts * 2 HP/heart = 10 HP
    const ENERGY_UPPER_BOUND: u64 = 10;
    const NAME_UPPER_BOUND: u64 = 40;
    const PARTS_SIZE: u64 = 3;
    const UNIT_PRICE: u64 = 100000000;

    struct MintAptogotchiEvents has key {
        mint_aptogotchi_events: event::EventHandle<MintAptogotchiEvent>,
    }

    struct MintAptogotchiEvent has drop, store {
        aptogotchi_name: String,
        parts: vector<u8>,
    }

    struct AptoGotchi has key {
        name: String,
        birthday: u64,
        energy_points: u64,
        parts: vector<u8>,
        mutator_ref: token::MutatorRef,
        burn_ref: token::BurnRef,
    }

    struct Accessory has key {
        category: String,
        id: u64,
    }

    // Tokens require a signer to create, so this is the signer for the collection
    struct CollectionCapability has key {
        capability: SignerCapability,
        burn_signer_capability: SignerCapability,
    }

    const APP_SIGNER_CAPABILITY_SEED: vector<u8> = b"APP_SIGNER_CAPABILITY";
    const BURN_SIGNER_CAPABILITY_SEED: vector<u8> = b"BURN_SIGNER_CAPABILITY";
    const APTOGOTCHI_COLLECTION_NAME: vector<u8> = b"Aptogotchi Collection";
    const APTOGOTCHI_COLLECTION_DESCRIPTION: vector<u8> = b"Aptogotchi Collection Description";
    const APTOGOTCHI_COLLECTION_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/aptogotchi.png";

    const ACCESSORY_COLLECTION_NAME: vector<u8> = b"Aptogotchi Accessory Collection";
    const ACCESSORY_COLLECTION_DESCRIPTION: vector<u8> = b"Aptogotchi Accessories";
    const ACCESSORY_COLLECTION_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/bowtie.png";

    const ACCESSORY_CATEGORY_BOWTIE: vector<u8> = b"bowtie";

    // This function is only callable during publishing
    fun init_module(account: &signer) {
        let (token_resource, token_signer_cap) = account::create_resource_account(
            account,
            APP_SIGNER_CAPABILITY_SEED,
        );
        let (_, burn_signer_capability) = account::create_resource_account(
            account,
            BURN_SIGNER_CAPABILITY_SEED,
        );

        move_to(account, CollectionCapability {
            capability: token_signer_cap,
            burn_signer_capability,
        });

        move_to(account, MintAptogotchiEvents {
            mint_aptogotchi_events: account::new_event_handle<MintAptogotchiEvent>(account),
        });

        create_aptogotchi_collection(&token_resource);
        create_accessory_collection(&token_resource);
    }

    fun get_token_signer(): signer acquires CollectionCapability {
        account::create_signer_with_capability(&borrow_global<CollectionCapability>(@aptogotchi).capability)
    }

    // Create the collection that will hold all the Aptogotchis
    fun create_aptogotchi_collection(creator: &signer) {
        let description = string::utf8(APTOGOTCHI_COLLECTION_DESCRIPTION);
        let name = string::utf8(APTOGOTCHI_COLLECTION_NAME);
        let uri = string::utf8(APTOGOTCHI_COLLECTION_URI);

        collection::create_unlimited_collection(
            creator,
            description,
            name,
            option::none(),
            uri,
        );
    }

    // Create the collection that will hold all the accessories
    fun create_accessory_collection(creator: &signer) {
        let description = string::utf8(ACCESSORY_COLLECTION_DESCRIPTION);
        let name = string::utf8(ACCESSORY_COLLECTION_NAME);
        let uri = string::utf8(ACCESSORY_COLLECTION_URI);

        collection::create_unlimited_collection(
            creator,
            description,
            name,
            option::none(),
            uri,
        );
    }

    // Create an Aptogotchi token object
    public entry fun create_aptogotchi(user: &signer, name: String, parts: vector<u8>) acquires CollectionCapability, MintAptogotchiEvents {
        assert!(vector::length(&parts)==PARTS_SIZE,error::invalid_argument(EPARTS_LIMIT));
        assert!(string::length(&name)<=NAME_UPPER_BOUND,error::invalid_argument(ENAME_LIMIT));

        let uri = string::utf8(APTOGOTCHI_COLLECTION_URI);
        let description = string::utf8(APTOGOTCHI_COLLECTION_DESCRIPTION);
        let user_addr = address_of(user);
        let token_name = to_string(&user_addr);

        assert!(!has_aptogotchi(user_addr), error::already_exists(EUSER_ALREADY_HAS_APTOGOTCHI));

        let constructor_ref = token::create_named_token(
            &get_token_signer(),
            string::utf8(APTOGOTCHI_COLLECTION_NAME),
            description,
            get_aptogotchi_token_name(&address_of(user)),
            option::none(),
            uri,
        );

        let token_signer = object::generate_signer(&constructor_ref);
        let mutator_ref = token::generate_mutator_ref(&constructor_ref);
        let burn_ref = token::generate_burn_ref(&constructor_ref);
        let transfer_ref = object::generate_transfer_ref(&constructor_ref);

        // initialize/set default Aptogotchi struct values
        let gotchi = AptoGotchi {
            name,
            birthday: timestamp::now_seconds(),
            energy_points: ENERGY_UPPER_BOUND,
            parts,
            mutator_ref,
            burn_ref,
        };

        move_to(&token_signer, gotchi);

        // Emit event for minting Aptogotchi token
        event::emit_event<MintAptogotchiEvent>(
            &mut borrow_global_mut<MintAptogotchiEvents>(@aptogotchi).mint_aptogotchi_events,
            MintAptogotchiEvent {
                aptogotchi_name: name,
                parts,
            },
        );

        object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), address_of(user));
    }

    // Get reference to Aptogotchi token object (CAN'T modify the reference)
    fun get_aptogotchi_address(creator_addr: &address): (address) acquires CollectionCapability {
        let collection = string::utf8(APTOGOTCHI_COLLECTION_NAME);
        let creator = &get_token_signer();
        let token_address = token::create_token_address(
            &signer::address_of(creator),
            &collection,
            &get_aptogotchi_token_name(creator_addr),
        );

        token_address
    }

    // Returns true if this address owns an Aptogotchi
    #[view]
    public fun has_aptogotchi(owner_addr: address): (bool) acquires CollectionCapability {
        let token_address = get_aptogotchi_address(&owner_addr);
        let has_gotchi = exists<AptoGotchi>(token_address);

        has_gotchi
    }

    // Returns all fields for this Aptogotchi (if found)
    #[view]
    public fun get_aptogotchi(owner_addr: address): (String, u64, u64, vector<u8>) acquires AptoGotchi, CollectionCapability {
        // if this address doesn't have an Aptogotchi, throw error
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<AptoGotchi>(token_address);

        // view function can only return primitive types.
        (gotchi.name, gotchi.birthday, gotchi.energy_points, gotchi.parts)
    }

    #[view]
    public fun get_energy_points(owner_addr: address): u64 acquires AptoGotchi, CollectionCapability {
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global<AptoGotchi>(token_address);

        gotchi.energy_points
    }

    public entry fun buy_food(owner: &signer, amount: u64) {
        // charge price for food
        coin::transfer<AptosCoin>(owner, @aptogotchi, UNIT_PRICE * amount);

        food::mint_food(owner, amount);
    }

    public entry fun feed(owner: &signer, points: u64) acquires AptoGotchi, CollectionCapability {
        let owner_addr = signer::address_of(owner);
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<AptoGotchi>(token_address);

        food::burn_food(owner, points);

        gotchi.energy_points = if (gotchi.energy_points + points > ENERGY_UPPER_BOUND) {
            ENERGY_UPPER_BOUND
        } else {
            gotchi.energy_points + points
        };

        gotchi.energy_points;
    }

    public entry fun play(owner: &signer, points: u64) acquires AptoGotchi, CollectionCapability {
        let owner_addr = signer::address_of(owner);
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<AptoGotchi>(token_address);

        gotchi.energy_points = if (gotchi.energy_points < points) {
            0
        } else {
            gotchi.energy_points - points
        };

        gotchi.energy_points;
    }

    // ==== ACCESSORIES ====
    // Create an Aptogotchi token object
    public entry fun create_accessory(user: &signer, category: String) acquires CollectionCapability {
        let uri = string::utf8(ACCESSORY_COLLECTION_URI);
        let description = string::utf8(ACCESSORY_COLLECTION_DESCRIPTION);

        let constructor_ref = token::create_named_token(
            &get_token_signer(),
            string::utf8(ACCESSORY_COLLECTION_NAME),
            description,
            get_accessory_token_name(&address_of(user), category),
            option::none(),
            uri,
        );

        let token_signer = object::generate_signer(&constructor_ref);
        let transfer_ref = object::generate_transfer_ref(&constructor_ref);
        let category = string::utf8(ACCESSORY_CATEGORY_BOWTIE);
        let id = 1;

        let accessory = Accessory {
            category,
            id,
        };

        move_to(&token_signer, accessory);
        object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), address_of(user));
    }

    public entry fun wear_accessory(owner: &signer, category: String) acquires CollectionCapability {
        let owner_addr = &address_of(owner);
        // retrieve the aptogotchi object
        let token_address = get_aptogotchi_address(owner_addr);
        let gotchi = object::address_to_object<AptoGotchi>(token_address);

        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);
        let accessory = object::address_to_object<Accessory>(accessory_address);

        object::transfer_to_object(owner, accessory, gotchi);
    }

    #[view]
    public fun has_accessory(owner: &signer, category: String): bool acquires CollectionCapability {
        let owner_addr = &address_of(owner);
        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);

        exists<Accessory>(accessory_address)
    }

    public entry fun unwear_accessory(owner: &signer, category: String) acquires CollectionCapability {
        let owner_addr = &address_of(owner);

        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);
        let has_accessory = exists<Accessory>(accessory_address);
        if (has_accessory == false) {
            assert!(false, error::unavailable(EACCESSORY_NOT_AVAILABLE));
        };
        let accessory = object::address_to_object<Accessory>(accessory_address);

        object::transfer(owner, accessory, signer::address_of(owner));
    }

    fun get_aptogotchi_token_name(owner_addr: &address): String {
        let token_name = utf8(b"aptogotchi");
        string::append(&mut token_name, to_string(owner_addr));

        token_name
    }

    fun get_accessory_token_name(owner_addr: &address, category: String): String {
        let token_name = category;
        string::append(&mut token_name, to_string(owner_addr));

        token_name
    }

    fun get_accessory_address(creator_addr: &address, category: String): (address) acquires CollectionCapability {
        let collection = string::utf8(ACCESSORY_COLLECTION_NAME);
        let token_name = category;
        string::append(&mut token_name, to_string(creator_addr));
        let creator = &get_token_signer();

        let token_address = token::create_token_address(
            &signer::address_of(creator),
            &collection,
            &get_accessory_token_name(creator_addr, category),
        );

        token_address
    }

ファンジブル・アセット

前回の記事でも言及したのですが、SuiやAptosはデータ管理の構造がEthereumと全く異なります。Suiにおいては生成されるデジタル・アセットの全てがNFTになっているといっても過言ではないでしょう。

でも全てがNFTのようにユニークなアセットとして表現されれば良いかというとそうでもないはずです。

ここではAptosにおけるファンジブルアセットの実装方法などについて学んでいきます!!

Aptogochiではエナジーポイントを取り扱うためにこのファンジブル・アセットの考え方が導入されています。

#[view]
public fun get_energy_points(owner_addr: address): u64 acquires AptoGotchi, CollectionCapability {
    assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

    let token_address = get_aptogotchi_address(&owner_addr);
    let gotchi = borrow_global<AptoGotchi>(token_address);

    gotchi.energy_points
}

public entry fun buy_food(owner: &signer, amount: u64) {
    // charge price for food
    coin::transfer<AptosCoin>(owner, @aptogotchi, UNIT_PRICE * amount);

    food::mint_food(owner, amount);
}

public entry fun feed(owner: &signer, points: u64) acquires AptoGotchi, CollectionCapability {
    let owner_addr = signer::address_of(owner);
    assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

    let token_address = get_aptogotchi_address(&owner_addr);
    let gotchi = borrow_global_mut<AptoGotchi>(token_address);

    food::burn_food(owner, points);

    gotchi.energy_points = if (gotchi.energy_points + points > ENERGY_UPPER_BOUND) {
        ENERGY_UPPER_BOUND
    } else {
        gotchi.energy_points + points
    };

    gotchi.energy_points;
}

public entry fun play(owner: &signer, points: u64) acquires AptoGotchi, CollectionCapability {
    let owner_addr = signer::address_of(owner);
    assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

    let token_address = get_aptogotchi_address(&owner_addr);
    let gotchi = borrow_global_mut<AptoGotchi>(token_address);

    gotchi.energy_points = if (gotchi.energy_points < points) {
        0
    } else {
        gotchi.energy_points - points
    };

    gotchi.energy_points;
}

ここで新しく food.moveというファイルを追加して以下内容をコピペしてください。

module aptogotchi::food {
    use aptos_framework::fungible_asset::{Self, MintRef, BurnRef};
    use aptos_framework::object::{Self, Object, ExtendRef};
    use aptos_framework::primary_fungible_store;
    use aptos_token_objects::collection;
    use aptos_token_objects::token;
    use std::option;
    use std::signer::address_of;
    use std::string::Self;
    // declare main module as a friend so only it can call mint_food and burn_food, but not other modules
    friend aptogotchi::main;

    const APP_OBJECT_SEED: vector<u8> = b"APTOGOTCHI_FOOD";
    /// The food collection name
    const FOOD_COLLECTION_NAME: vector<u8> = b"Food Collection Name";
    /// The food collection description
    const FOOD_COLLECTION_DESCRIPTION: vector<u8> = b"Food Collection Description";
    /// The food collection URI
    const FOOD_COLLECTION_URI: vector<u8> = b"https://food.collection.uri";
    const FOOD_DESCRIPTION: vector<u8> = b"Food Description";
    const FOOD_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/food.png";
    const FOOD_NAME: vector<u8> = b"Food";
    const FOOD_SYMBOL: vector<u8> = b"FOOD";
    const PROJECT_URI: vector<u8> = b"https://www.aptoslabs.com";

    // We need a contract signer as the creator of the food collection and food token
    // Otherwise we need admin to sign whenever a new food token is minted which is inconvenient
    struct ObjectController has key {
        // This is the extend_ref of the app object, not the extend_ref of food collection object or food token object
        // app object is the creator and owner of food collection object
        // app object is also the creator of all food token (ERC-1155 like semi fungible token) objects
        // but owner of each food token object is aptogotchi owner
        app_extend_ref: ExtendRef,
    }

    struct FoodToken has key {
        /// Used to mint fungible assets.
        fungible_asset_mint_ref: MintRef,
        /// Used to burn fungible assets.
        fungible_asset_burn_ref: BurnRef,
    }

    fun init_module(account: &signer) {
        let constructor_ref = &object::create_named_object(account, APP_OBJECT_SEED);
        let extend_ref = object::generate_extend_ref(constructor_ref);
        let app_signer = &object::generate_signer(constructor_ref);

        move_to(app_signer, ObjectController {
            app_extend_ref: extend_ref,
        });

        create_food_collection(app_signer);
        create_food_token(app_signer);
    }

    fun get_app_signer_address(): address {
        object::create_object_address(&@aptogotchi, APP_OBJECT_SEED)
    }

    fun get_app_signer(app_signer_address: address): signer acquires ObjectController {
        object::generate_signer_for_extending(&borrow_global<ObjectController>(app_signer_address).app_extend_ref)
    }

    /// Creates the food collection.
    fun create_food_collection(creator: &signer) {
        collection::create_unlimited_collection(
            creator,
            string::utf8(FOOD_COLLECTION_DESCRIPTION),
            string::utf8(FOOD_COLLECTION_NAME),
            option::none(),
            string::utf8(FOOD_COLLECTION_URI),
        );
    }

    /// Creates the food token as fungible token.
    fun create_food_token(creator: &signer) {
        let constructor_ref = &token::create_named_token(
            creator,
            string::utf8(FOOD_COLLECTION_NAME),
            string::utf8(FOOD_DESCRIPTION),
            string::utf8(FOOD_NAME),
            option::none(),
            string::utf8(FOOD_URI),
        );

        // Creates the fungible asset.
        primary_fungible_store::create_primary_store_enabled_fungible_asset(
            constructor_ref,
            option::none(),
            string::utf8(FOOD_NAME),
            string::utf8(FOOD_SYMBOL),
            0,
            string::utf8(FOOD_URI),
            string::utf8(PROJECT_URI),
        );
        let fungible_asset_mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
        let fungible_asset_burn_ref = fungible_asset::generate_burn_ref(constructor_ref);

        let food_token_signer = &object::generate_signer(constructor_ref);
        // Publishes the FoodToken resource with the refs.
        move_to(food_token_signer, FoodToken {
            fungible_asset_mint_ref,
            fungible_asset_burn_ref,
        });
    }

    public(friend) fun mint_food(user: &signer, amount: u64) acquires FoodToken {
        let food_token = borrow_global<FoodToken>(get_food_token_address());
        let fungible_asset_mint_ref = &food_token.fungible_asset_mint_ref;
        primary_fungible_store::deposit(
            address_of(user),
            fungible_asset::mint(fungible_asset_mint_ref, amount),
        );
    }

    public(friend) fun burn_food(user: &signer, amount: u64) acquires FoodToken {
        let food_token = borrow_global<FoodToken>(get_food_token_address());
        primary_fungible_store::burn(&food_token.fungible_asset_burn_ref, address_of(user), amount);
    }

    #[view]
    public fun get_food_token_address(): address {
        token::create_token_address(
            &get_app_signer_address(),
            &string::utf8(FOOD_COLLECTION_NAME),
            &string::utf8(FOOD_NAME),
        )
    }

    #[view]
    /// Returns the balance of the food token of the owner
    public fun food_balance(owner_addr: address, food: Object<FoodToken>): u64 {
        // should remove this function when re-publish the contract to the final address
        // this function is replaced by get_food_balance
        primary_fungible_store::balance(owner_addr, food)
    }

    #[view]
    /// Returns the balance of the food token of the owner
    public fun get_food_balance(owner_addr: address): u64 {
        let food_token = object::address_to_object<FoodToken>(get_food_token_address());
        primary_fungible_store::balance(owner_addr, food_token)
    }

    #[test_only]
    use aptos_framework::account::create_account_for_test;

    #[test_only]
    public fun init_module_for_test(creator: &signer) {
        init_module(creator);
    }

    #[test(account = @aptogotchi, creator = @0x123)]
    fun test_food(account: &signer, creator: &signer) acquires FoodToken {
        init_module(account);
        create_account_for_test(address_of(creator));

        mint_food(creator, 1);
        assert!(get_food_balance(address_of(creator)) == 1, 0);

        burn_food(creator, 1);
        assert!(get_food_balance(address_of(creator)) == 0, 0);
    }
}

Aptogochiを生成する時と似たような流れになっていますね。

ファンジブル・アセット用のモジュールであるfungible_assetには、次のようなメソッドが実装されているとのことです。

具体的には下記部分にて使用されています。

 let fungible_asset_mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
        let fungible_asset_burn_ref = fungible_asset::generate_burn_ref(constructor_ref);

        let food_token_signer = &object::generate_signer(constructor_ref);
        // Publishes the FoodToken resource with the refs.
        move_to(food_token_signer, FoodToken {
            fungible_asset_mint_ref,
            fungible_asset_burn_ref,
        });

ファンジブルアセットの残高を取得したい時は次のように実装するそうです。

#[view]
/// Returns the balance of the food token of the owner
public fun get_food_balance(owner_addr: address): u64 {
    let food_token = object::address_to_object<FoodToken>(get_food_token_address());
    primary_fungible_store::balance(owner_addr, food_token)
}

また、ファンジブルトークンには価格を設定できるみたいですね!!

public entry fun buy_food(owner: &signer, amount: u64) {
    // charge price for food
    coin::transfer<AptosCoin>(owner, @aptogotchi, UNIT_PRICE * amount);

    food::mint_food(owner, amount);
}

Solidityなどと比較してもかなり柔軟な設計が可能なことが伺えます。

オブジェクトモデル

ObjectはAptos Moveのコアプリミティブで、0x1::objectのObjectモジュールで作成されます。

ObjectはDigital Asset StandardFungible Asset Standardをサポートしています。

ここまで学習してきたやつですね。

オブジェクトは、1つのアドレス内に格納されるリソースのコンテナです。

これらのリソースは通常、一緒にアクセスされることが多い関連データを表し、データのローカリティとコスト削減のために単一のアドレス内に格納する必要があります。

オブジェクトの作成関数はすべて、格納できない一時的な ConstructorRef を返します。ConstructorRefを使用すると、オブジェクトにリソースを追加できます。
ConstructorRefは、オブジェクトの生成にも使用できます。

オブジェクトを管理するためによく使う機能が他にもあります。

例えば以下のようなメソッドがあります。

/// remove Object from global storage.
public fun generate_delete_ref(ref: &ConstructorRef): DeleteRef;

/// add new events and resources to the object.
public fun generate_extend_ref(ref: &ConstructorRef): ExtendRef;

/// Generates the TransferRef, which can be used to manage object transfers.
public fun generate_transfer_ref(ref: &ConstructorRef): TransferRef;

/// Create a signer for the ConstructorRef
public fun generate_signer(ref: &ConstructorRef): signer;

ここまでのソースコード

さてここまでのソースコードを確認していきます!!

まず、aptogochi.moveファイルですが以下のようになっていればOKです!!

module aptogotchi::main {
    use aptogotchi::food;
    use aptos_framework::aptos_coin::AptosCoin;
    use aptos_framework::coin;
    use aptos_framework::event;
    use aptos_framework::object::{Self, ExtendRef};
    use aptos_framework::timestamp;
    use aptos_std::string_utils::{to_string};
    use aptos_token_objects::collection;
    use aptos_token_objects::token;
    use std::error;
    use std::option;
    use std::signer::address_of;
    use std::string::{Self, String};
    use std::vector;

    /// aptogotchi not available
    const ENOT_AVAILABLE: u64 = 1;
    /// accessory not available
    const EACCESSORY_NOT_AVAILABLE: u64 = 1;
    const EPARTS_LIMIT: u64 = 2;
    const ENAME_LIMIT: u64 = 3;
    const EUSER_ALREADY_HAS_APTOGOTCHI: u64 = 4;


    // maximum health points: 5 hearts * 2 HP/heart = 10 HP
    const ENERGY_UPPER_BOUND: u64 = 10;
    const NAME_UPPER_BOUND: u64 = 40;
    const PARTS_SIZE: u64 = 3;
    const UNIT_PRICE: u64 = 100000000;

    #[event]
    struct MintAptogotchiEvent has drop, store {
        aptogotchi_name: String,
        parts: vector<u8>,
    }

    struct Aptogotchi has key {
        name: String,
        birthday: u64,
        energy_points: u64,
        parts: vector<u8>,
        mutator_ref: token::MutatorRef,
        burn_ref: token::BurnRef,
    }

    struct Accessory has key {
        category: String,
        id: u64,
    }

    // We need a contract signer as the creator of the aptogotchi collection and aptogotchi token
    // Otherwise we need admin to sign whenever a new aptogotchi token is minted which is inconvenient
    struct ObjectController has key {
        // This is the extend_ref of the app object, not the extend_ref of collection object or token object
        // app object is the creator and owner of aptogotchi collection object
        // app object is also the creator of all aptogotchi token (NFT) objects
        // but owner of each token object is aptogotchi owner (i.e. user who mints aptogotchi)
        app_extend_ref: ExtendRef,
    }

    const APP_OBJECT_SEED: vector<u8> = b"APTOGOTCHI";
    const APTOGOTCHI_COLLECTION_NAME: vector<u8> = b"Aptogotchi Collection";
    const APTOGOTCHI_COLLECTION_DESCRIPTION: vector<u8> = b"Aptogotchi Collection Description";
    const APTOGOTCHI_COLLECTION_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/aptogotchi.png";

    const ACCESSORY_COLLECTION_NAME: vector<u8> = b"Aptogotchi Accessory Collection";
    const ACCESSORY_COLLECTION_DESCRIPTION: vector<u8> = b"Aptogotchi Accessories";
    const ACCESSORY_COLLECTION_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/bowtie.png";

    const ACCESSORY_CATEGORY_BOWTIE: vector<u8> = b"bowtie";

    // This function is only callable during publishing
    fun init_module(account: &signer) {
        let constructor_ref = &object::create_named_object(account, APP_OBJECT_SEED);
        let extend_ref = object::generate_extend_ref(constructor_ref);
        let app_signer = &object::generate_signer(constructor_ref);

        move_to(app_signer, ObjectController {
            app_extend_ref: extend_ref,
        });

        create_aptogotchi_collection(app_signer);
        create_accessory_collection(app_signer);
    }

    fun get_app_signer_address(): address {
        object::create_object_address(&@aptogotchi, APP_OBJECT_SEED)
    }

    fun get_app_signer(app_signer_address: address): signer acquires ObjectController {
        object::generate_signer_for_extending(&borrow_global<ObjectController>(app_signer_address).app_extend_ref)
    }

    // Create the collection that will hold all the Aptogotchis
    fun create_aptogotchi_collection(creator: &signer) {
        let description = string::utf8(APTOGOTCHI_COLLECTION_DESCRIPTION);
        let name = string::utf8(APTOGOTCHI_COLLECTION_NAME);
        let uri = string::utf8(APTOGOTCHI_COLLECTION_URI);

        collection::create_unlimited_collection(
            creator,
            description,
            name,
            option::none(),
            uri,
        );
    }

    // Create the collection that will hold all the accessories
    fun create_accessory_collection(creator: &signer) {
        let description = string::utf8(ACCESSORY_COLLECTION_DESCRIPTION);
        let name = string::utf8(ACCESSORY_COLLECTION_NAME);
        let uri = string::utf8(ACCESSORY_COLLECTION_URI);

        collection::create_unlimited_collection(
            creator,
            description,
            name,
            option::none(),
            uri,
        );
    }

    // Create an Aptogotchi token object
    public entry fun create_aptogotchi(user: &signer, name: String, parts: vector<u8>) acquires ObjectController {
        assert!(vector::length(&parts) == PARTS_SIZE, error::invalid_argument(EPARTS_LIMIT));
        assert!(string::length(&name) <= NAME_UPPER_BOUND, error::invalid_argument(ENAME_LIMIT));
        let uri = string::utf8(APTOGOTCHI_COLLECTION_URI);
        let description = string::utf8(APTOGOTCHI_COLLECTION_DESCRIPTION);
        let user_addr = address_of(user);
        assert!(!has_aptogotchi(user_addr), error::already_exists(EUSER_ALREADY_HAS_APTOGOTCHI));

        let constructor_ref = token::create_named_token(
            &get_app_signer(get_app_signer_address()),
            string::utf8(APTOGOTCHI_COLLECTION_NAME),
            description,
            get_aptogotchi_token_name(&address_of(user)),
            option::none(),
            uri,
        );

        let token_signer = object::generate_signer(&constructor_ref);
        let mutator_ref = token::generate_mutator_ref(&constructor_ref);
        let burn_ref = token::generate_burn_ref(&constructor_ref);
        let transfer_ref = object::generate_transfer_ref(&constructor_ref);

        // initialize/set default Aptogotchi struct values
        let gotchi = Aptogotchi {
            name,
            birthday: timestamp::now_seconds(),
            energy_points: ENERGY_UPPER_BOUND,
            parts,
            mutator_ref,
            burn_ref,
        };

        move_to(&token_signer, gotchi);

        // Emit event for minting Aptogotchi token
        event::emit<MintAptogotchiEvent>(
            MintAptogotchiEvent {
                aptogotchi_name: name,
                parts,
            },
        );

        object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), address_of(user));
    }

    // Get reference to Aptogotchi token object (CAN'T modify the reference)
    fun get_aptogotchi_address(creator_addr: &address): (address) {
        let token_address = token::create_token_address(
            &get_app_signer_address(),
            &string::utf8(APTOGOTCHI_COLLECTION_NAME),
            &get_aptogotchi_token_name(creator_addr),
        );

        token_address
    }

    // Returns true if this address owns an Aptogotchi
    #[view]
    public fun has_aptogotchi(owner_addr: address): (bool) {
        let token_address = get_aptogotchi_address(&owner_addr);

        exists<Aptogotchi>(token_address)
    }

    // Returns all fields for this Aptogotchi (if found)
    #[view]
    public fun get_aptogotchi(owner_addr: address): (String, u64, u64, vector<u8>) acquires Aptogotchi {
        // if this address doesn't have an Aptogotchi, throw error
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<Aptogotchi>(token_address);

        // view function can only return primitive types.
        (gotchi.name, gotchi.birthday, gotchi.energy_points, gotchi.parts)
    }

    #[view]
    public fun get_energy_points(owner_addr: address): u64 acquires Aptogotchi {
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global<Aptogotchi>(token_address);

        gotchi.energy_points
    }

    public entry fun buy_food(owner: &signer, amount: u64) {
        // charge price for food
        coin::transfer<AptosCoin>(owner, @aptogotchi, UNIT_PRICE * amount);
        food::mint_food(owner, amount);
    }

    public entry fun feed(owner: &signer, points: u64) acquires Aptogotchi {
        let owner_addr = address_of(owner);
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<Aptogotchi>(token_address);

        food::burn_food(owner, points);

        gotchi.energy_points = if (gotchi.energy_points + points > ENERGY_UPPER_BOUND) {
            ENERGY_UPPER_BOUND
        } else {
            gotchi.energy_points + points
        };

        gotchi.energy_points;
    }

    public entry fun play(owner: &signer, points: u64) acquires Aptogotchi {
        let owner_addr = address_of(owner);
        assert!(has_aptogotchi(owner_addr), error::unavailable(ENOT_AVAILABLE));

        let token_address = get_aptogotchi_address(&owner_addr);
        let gotchi = borrow_global_mut<Aptogotchi>(token_address);

        gotchi.energy_points = if (gotchi.energy_points < points) {
            0
        } else {
            gotchi.energy_points - points
        };

        gotchi.energy_points;
    }

    // ==== ACCESSORIES ====
    // Create an Aptogotchi token object
    public entry fun create_accessory(user: &signer, category: String) acquires ObjectController {
        let uri = string::utf8(ACCESSORY_COLLECTION_URI);
        let description = string::utf8(ACCESSORY_COLLECTION_DESCRIPTION);

        let constructor_ref = token::create_named_token(
            &get_app_signer(get_app_signer_address()),
            string::utf8(ACCESSORY_COLLECTION_NAME),
            description,
            get_accessory_token_name(&address_of(user), category),
            option::none(),
            uri,
        );

        let token_signer = object::generate_signer(&constructor_ref);
        let transfer_ref = object::generate_transfer_ref(&constructor_ref);
        let category = string::utf8(ACCESSORY_CATEGORY_BOWTIE);
        let id = 1;

        let accessory = Accessory {
            category,
            id,
        };

        move_to(&token_signer, accessory);
        object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), address_of(user));
    }

    public entry fun wear_accessory(owner: &signer, category: String) acquires ObjectController {
        let owner_addr = &address_of(owner);
        // retrieve the aptogotchi object
        let token_address = get_aptogotchi_address(owner_addr);
        let gotchi = object::address_to_object<Aptogotchi>(token_address);

        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);
        let accessory = object::address_to_object<Accessory>(accessory_address);

        object::transfer_to_object(owner, accessory, gotchi);
    }

    #[view]
    public fun has_accessory(owner: &signer, category: String): bool acquires ObjectController {
        let owner_addr = &address_of(owner);
        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);

        exists<Accessory>(accessory_address)
    }

    public entry fun unwear_accessory(owner: &signer, category: String) acquires ObjectController {
        let owner_addr = &address_of(owner);

        // retrieve the accessory object by category
        let accessory_address = get_accessory_address(owner_addr, category);
        let has_accessory = exists<Accessory>(accessory_address);
        if (has_accessory == false) {
            assert!(false, error::unavailable(EACCESSORY_NOT_AVAILABLE));
        };
        let accessory = object::address_to_object<Accessory>(accessory_address);

        object::transfer(owner, accessory, address_of(owner));
    }

    fun get_aptogotchi_token_name(owner_addr: &address): String {
        let token_name = string::utf8(b"aptogotchi");
        string::append(&mut token_name, to_string(owner_addr));

        token_name
    }

    fun get_accessory_token_name(owner_addr: &address, category: String): String {
        let token_name = category;
        string::append(&mut token_name, to_string(owner_addr));

        token_name
    }

    fun get_accessory_address(creator_addr: &address, category: String): (address) acquires ObjectController {
        let collection = string::utf8(ACCESSORY_COLLECTION_NAME);
        let token_name = category;
        string::append(&mut token_name, to_string(creator_addr));
        let creator = &get_app_signer(get_app_signer_address());

        let token_address = token::create_token_address(
            &address_of(creator),
            &collection,
            &get_accessory_token_name(creator_addr, category),
        );

        token_address
    }

    // ==== TESTS ====
    // Setup testing environment
    #[test_only]
    use aptos_framework::account::create_account_for_test;
    #[test_only]
    use aptos_framework::aptos_coin;

    #[test_only]
    fun setup_test(aptos: &signer, account: &signer, creator: &signer) {
        let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(aptos);

        // create fake accounts (only for testing purposes) and deposit initial balance

        create_account_for_test(address_of(account));
        coin::register<AptosCoin>(account);

        let creator_addr = address_of(creator);
        create_account_for_test(address_of(creator));
        coin::register<AptosCoin>(creator);
        let coins = coin::mint(3 * UNIT_PRICE, &mint_cap);
        coin::deposit(creator_addr, coins);

        coin::destroy_burn_cap(burn_cap);
        coin::destroy_mint_cap(mint_cap);

        timestamp::set_time_has_started_for_testing(aptos);
        init_module(account);
    }

    // Test creating an Aptogotchi
    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    fun test_create_aptogotchi(aptos: &signer, account: &signer, creator: &signer) acquires ObjectController {
        setup_test(aptos, account, creator);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);

        let has_aptogotchi = has_aptogotchi(address_of(creator));
        assert!(has_aptogotchi, 1);
    }

    // Test getting an Aptogotchi, when user has not minted
    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    #[expected_failure(abort_code = 851969, location = aptogotchi::main)]
    fun test_get_aptogotchi_without_creation(
        aptos: &signer,
        account: &signer,
        creator: &signer
    ) acquires Aptogotchi {
        setup_test(aptos, account, creator);

        // get aptogotchi without creating it
        get_aptogotchi(address_of(creator));
    }

    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    fun test_feed_and_play(aptos: &signer, account: &signer, creator: &signer) acquires ObjectController, Aptogotchi {
        setup_test(aptos, account, creator);
        food::init_module_for_test(account);

        let creator_addr = address_of(creator);
        let account_addr = address_of(account);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
        assert!(get_energy_points(creator_addr) == ENERGY_UPPER_BOUND, 1);

        play(creator, 5);
        assert!(get_energy_points(creator_addr) == ENERGY_UPPER_BOUND - 5, 1);

        assert!(coin::balance<AptosCoin>(creator_addr) == 3 * UNIT_PRICE, 1);
        assert!(coin::balance<AptosCoin>(account_addr) == 0, 1);
        buy_food(creator, 3);
        assert!(coin::balance<AptosCoin>(creator_addr) == 0, 1);
        assert!(coin::balance<AptosCoin>(account_addr) == 3 * UNIT_PRICE, 1);
        feed(creator, 3);
        assert!(get_energy_points(address_of(creator)) == ENERGY_UPPER_BOUND - 2, 1);
    }

    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    #[expected_failure(abort_code = 393218, location = 0x1::object)]
    fun test_feed_with_no_food(aptos: &signer, account: &signer, creator: &signer) acquires ObjectController, Aptogotchi {
        setup_test(aptos, account, creator);
        food::init_module_for_test(account);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
        assert!(get_energy_points(address_of(creator)) == ENERGY_UPPER_BOUND, 1);

        play(creator, 5);
        assert!(get_energy_points(address_of(creator)) == ENERGY_UPPER_BOUND - 5, 1);

        feed(creator, 3);
        assert!(get_energy_points(address_of(creator)) == ENERGY_UPPER_BOUND - 2, 1);
    }

    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    fun test_create_accessory(aptos: &signer, account: &signer, creator: &signer) acquires ObjectController, Accessory {
        setup_test(aptos, account, creator);
        let creator_address = &address_of(creator);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
        create_accessory(creator, string::utf8(ACCESSORY_CATEGORY_BOWTIE));
        let accessory_address = get_accessory_address(creator_address, string::utf8(ACCESSORY_CATEGORY_BOWTIE));

        let accessory = borrow_global<Accessory>(accessory_address);

        assert!(accessory.category == string::utf8(ACCESSORY_CATEGORY_BOWTIE), 1);
    }

    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    fun test_wear_accessory(aptos: &signer, account: &signer, creator: &signer) acquires ObjectController {
        setup_test(aptos, account, creator);
        let creator_address = &address_of(creator);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
        create_accessory(creator, string::utf8(ACCESSORY_CATEGORY_BOWTIE));
        let accessory_address = get_accessory_address(creator_address, string::utf8(ACCESSORY_CATEGORY_BOWTIE));
        let aptogotchi_address = get_aptogotchi_address(creator_address);

        let accessory_obj = object::address_to_object<Accessory>(accessory_address);
        assert!(object::is_owner(accessory_obj, address_of(creator)), 2);

        wear_accessory(creator, string::utf8(ACCESSORY_CATEGORY_BOWTIE));
        assert!(object::is_owner(accessory_obj, aptogotchi_address), 3);

        unwear_accessory(creator, string::utf8(ACCESSORY_CATEGORY_BOWTIE));
        assert!(object::is_owner(accessory_obj, address_of(creator)), 4);
    }

    // Test getting an Aptogotchi, when user has not minted
    #[test(aptos = @0x1, account = @aptogotchi, creator = @0x123)]
    #[expected_failure(abort_code = 524292, location = aptogotchi::main)]
    fun test_create_aptogotchi_twice(
        aptos: &signer,
        account: &signer,
        creator: &signer
    ) acquires ObjectController {
        setup_test(aptos, account, creator);

        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
        create_aptogotchi(creator, string::utf8(b"test"), vector[1, 1, 1]);
    }
}

次に food.moveファイルですが以下のようになっていればOKです!!

module aptogotchi::food {
    use aptos_framework::fungible_asset::{Self, MintRef, BurnRef};
    use aptos_framework::object::{Self, Object, ExtendRef};
    use aptos_framework::primary_fungible_store;
    use aptos_token_objects::collection;
    use aptos_token_objects::token;
    use std::option;
    use std::signer::address_of;
    use std::string::Self;
    // declare main module as a friend so only it can call mint_food and burn_food, but not other modules
    friend aptogotchi::main;

    const APP_OBJECT_SEED: vector<u8> = b"APTOGOTCHI_FOOD";
    /// The food collection name
    const FOOD_COLLECTION_NAME: vector<u8> = b"Food Collection Name";
    /// The food collection description
    const FOOD_COLLECTION_DESCRIPTION: vector<u8> = b"Food Collection Description";
    /// The food collection URI
    const FOOD_COLLECTION_URI: vector<u8> = b"https://food.collection.uri";
    const FOOD_DESCRIPTION: vector<u8> = b"Food Description";
    const FOOD_URI: vector<u8> = b"https://otjbxblyfunmfblzdegw.supabase.co/storage/v1/object/public/aptogotchi/food.png";
    const FOOD_NAME: vector<u8> = b"Food";
    const FOOD_SYMBOL: vector<u8> = b"FOOD";
    const PROJECT_URI: vector<u8> = b"https://www.aptoslabs.com";

    // We need a contract signer as the creator of the food collection and food token
    // Otherwise we need admin to sign whenever a new food token is minted which is inconvenient
    struct ObjectController has key {
        // This is the extend_ref of the app object, not the extend_ref of food collection object or food token object
        // app object is the creator and owner of food collection object
        // app object is also the creator of all food token (ERC-1155 like semi fungible token) objects
        // but owner of each food token object is aptogotchi owner
        app_extend_ref: ExtendRef,
    }

    struct FoodToken has key {
        /// Used to mint fungible assets.
        fungible_asset_mint_ref: MintRef,
        /// Used to burn fungible assets.
        fungible_asset_burn_ref: BurnRef,
    }

    fun init_module(account: &signer) {
        let constructor_ref = &object::create_named_object(account, APP_OBJECT_SEED);
        let extend_ref = object::generate_extend_ref(constructor_ref);
        let app_signer = &object::generate_signer(constructor_ref);

        move_to(app_signer, ObjectController {
            app_extend_ref: extend_ref,
        });

        create_food_collection(app_signer);
        create_food_token(app_signer);
    }

    fun get_app_signer_address(): address {
        object::create_object_address(&@aptogotchi, APP_OBJECT_SEED)
    }

    fun get_app_signer(app_signer_address: address): signer acquires ObjectController {
        object::generate_signer_for_extending(&borrow_global<ObjectController>(app_signer_address).app_extend_ref)
    }

    /// Creates the food collection.
    fun create_food_collection(creator: &signer) {
        collection::create_unlimited_collection(
            creator,
            string::utf8(FOOD_COLLECTION_DESCRIPTION),
            string::utf8(FOOD_COLLECTION_NAME),
            option::none(),
            string::utf8(FOOD_COLLECTION_URI),
        );
    }

    /// Creates the food token as fungible token.
    fun create_food_token(creator: &signer) {
        let constructor_ref = &token::create_named_token(
            creator,
            string::utf8(FOOD_COLLECTION_NAME),
            string::utf8(FOOD_DESCRIPTION),
            string::utf8(FOOD_NAME),
            option::none(),
            string::utf8(FOOD_URI),
        );

        // Creates the fungible asset.
        primary_fungible_store::create_primary_store_enabled_fungible_asset(
            constructor_ref,
            option::none(),
            string::utf8(FOOD_NAME),
            string::utf8(FOOD_SYMBOL),
            0,
            string::utf8(FOOD_URI),
            string::utf8(PROJECT_URI),
        );
        let fungible_asset_mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
        let fungible_asset_burn_ref = fungible_asset::generate_burn_ref(constructor_ref);

        let food_token_signer = &object::generate_signer(constructor_ref);
        // Publishes the FoodToken resource with the refs.
        move_to(food_token_signer, FoodToken {
            fungible_asset_mint_ref,
            fungible_asset_burn_ref,
        });
    }

    public(friend) fun mint_food(user: &signer, amount: u64) acquires FoodToken {
        let food_token = borrow_global<FoodToken>(get_food_token_address());
        let fungible_asset_mint_ref = &food_token.fungible_asset_mint_ref;
        primary_fungible_store::deposit(
            address_of(user),
            fungible_asset::mint(fungible_asset_mint_ref, amount),
        );
    }

    public(friend) fun burn_food(user: &signer, amount: u64) acquires FoodToken {
        let food_token = borrow_global<FoodToken>(get_food_token_address());
        primary_fungible_store::burn(&food_token.fungible_asset_burn_ref, address_of(user), amount);
    }

    #[view]
    public fun get_food_token_address(): address {
        token::create_token_address(
            &get_app_signer_address(),
            &string::utf8(FOOD_COLLECTION_NAME),
            &string::utf8(FOOD_NAME),
        )
    }

    #[view]
    /// Returns the balance of the food token of the owner
    public fun food_balance(owner_addr: address, food: Object<FoodToken>): u64 {
        // should remove this function when re-publish the contract to the final address
        // this function is replaced by get_food_balance
        primary_fungible_store::balance(owner_addr, food)
    }

    #[view]
    /// Returns the balance of the food token of the owner
    public fun get_food_balance(owner_addr: address): u64 {
        let food_token = object::address_to_object<FoodToken>(get_food_token_address());
        primary_fungible_store::balance(owner_addr, food_token)
    }

    #[test_only]
    use aptos_framework::account::create_account_for_test;

    #[test_only]
    public fun init_module_for_test(creator: &signer) {
        init_module(creator);
    }

    #[test(account = @aptogotchi, creator = @0x123)]
    fun test_food(account: &signer, creator: &signer) acquires FoodToken {
        init_module(account);
        create_account_for_test(address_of(creator));

        mint_food(creator, 1);
        assert!(get_food_balance(address_of(creator)) == 1, 0);

        burn_food(creator, 1);
        assert!(get_food_balance(address_of(creator)) == 0, 0);
    }
}

そして scripts/script_create_gotchi.moveファイルも若干修正が必要なので以下のように修正してください!!

script {
    use std::string::utf8;

    fun create_gotchi(user: &signer) {
        let gotchi_name = utf8(b"gotchi");
        aptogotchi::main::create_aptogotchi(user, gotchi_name, vector[1, 1, 1]);
    }
}

ここまで完了したらビルドしましょう!!

aptos move build

問題なければ以下のように出力されるはずです!!

UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY AptosTokenObjects
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptogotchi
{
  "Result": [
    "9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::food",
    "9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main"
  ]
}

次にテストもやっておきましょう!!

aptos move test

問題なければ全てのユニットテストが正常終了するはずです!!

INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY AptosTokenObjects
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptogotchi
Running Move unit tests
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::food::test_food
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_create_accessory
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_create_aptogotchi
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_create_aptogotchi_twice
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_feed_and_play
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_feed_with_no_food
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_get_aptogotchi_without_creation
[ PASS    ] 0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee::main::test_wear_accessory
Test result: OK. Total tests: 8; passed: 8; failed: 0
{
  "Result": "Success"
}

ではテストまでうまくいったのでデプロイしてみます!!

aptos move publish --named-addresses aptogotchi=mashharuki --profile=mashharuki --url https://fullnode.testnet.aptoslabs.com/v1

無事にテストネットにデプロイされました!!

ompiling, may take a little while to download git dependencies...
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY AptosTokenObjects
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptogotchi
package size 11710 bytes
Do you want to submit a transaction for a range of [1103400 - 1655100] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
{
  "Result": {
    "transaction_hash": "0x28e861e8ebe698594d747d7e878dbf2cfd74fe6a1343ace56568f18468a8d563",
    "gas_used": 11034,
    "gas_unit_price": 100,
    "sender": "9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee",
    "sequence_number": 7,
    "success": true,
    "timestamp_us": 1710570431222205,
    "version": 969153054,
    "vm_status": "Executed successfully"
  }
}

デプロイした時のトランザクションは下記で確認ができます!!

https://explorer.aptoslabs.com/txn/0x28e861e8ebe698594d747d7e878dbf2cfd74fe6a1343ace56568f18468a8d563?network=testnet

デプロイされたスマートコントラクトのソースコードは下記で確認できます!!

https://explorer.aptoslabs.com/account/0x9324da379576a6210929ee2611cd1995a21451d0be44ec92a92d17cae1d665ee/modules/code/food?network=testnet

スマートコントラクト側はとりあえずこれでOKです!!!

フロント側もソースコードが色々変わっているので下記を参考に前回から各コンポーネントファイルを更新してください!!

https://github.com/aptos-labs/aptogotchi-intermediate/blob/main/frontend

更新が終わったらビルドコマンドを実行して問題なくビルドされるか試してみましょう!!

yarn build

以下のようになればOKです!!

Route (app)                                Size     First Load JS
─ ○ /                                      21.6 kB         200 kB
+ First Load JS shared by all              179 kB
  ├ chunks/2267893a-04d9994a89532f8b.js    21.1 kB
  ├ chunks/437-3f38d81376993395.js         25.7 kB
  ├ chunks/794-ee75fc218f9574e6.js         79.5 kB
  ├ chunks/d909b7fe-0e182e62c4d25929.js    50.6 kB
  ├ chunks/main-app-9b3aa73a2ee4256d.js    214 B
  └ chunks/webpack-96c4165d76e930bc.js     1.74 kB

Route (pages)                              Size     First Load JS
─ ○ /404                                   181 B          75.7 kB
+ First Load JS shared by all              75.5 kB
  ├ chunks/framework-510ec8ffd65e1d01.js   45 kB
  ├ chunks/main-496fbdd0b468e353.js        28.5 kB
  ├ chunks/pages/_app-4fa603cb0fa6e977.js  195 B
  └ chunks/webpack-96c4165d76e930bc.js     1.74 kB

○  (Static)  automatically rendered as static HTML (uses no initial props)

✨  Done in 27.94s.

さぁ、これでフロントを起動する準備が整いました!!!

早速遊んでみましょう!!!

以下コマンドで実行してみます!!

yarn deploy

テストネットに接続していないと怒られます・・。

テストネットに接続し直すと最初にAptogochiの初期キャラを選択できます!!

選ぶと画面が切り替わります!!
前と比べてボタンを増えました!!!

エナジー補給のためにfoodを買ったりアクセサリーを買ったりすることができるようになりました!!!

今回はここまでになります!!!

読んでいただきありがとうございました!!

Discussion