最简带属性 NFT 合约实现 | Move 全链游戏开发(贰拾柒)
Leeduckgo
2024-03-20 21:58
订阅此专栏
收藏此文章

从本文开始,我们将以 Aptos 为基础,Step by Step 的阐述如何在 Move 上进行全链游戏的开发。

今天我们会从一个简单的 NFT 合约开始。麻雀虽小,五脏俱全,这个合约里面会涉及如下特性:

  • SignerCapability —— 账户操作权限
  • NFT Collection & NFT Token —— 对 NFT 合集与 NFT Token 的操作
  • NFT Properties —— 如何正确的管理 NFT 的属性
  • Fun, Entry Fun & View Fun —— 不同类型的函数

Hero 的完整代码如下:

module hero::hero {
    use aptos_framework::account::{Self, SignerCapability};

    use std::option;
    use std::signer;
    use std::string::{SelfString};

    use aptos_framework::object::{Self, ConstructorRef, Object};

    use aptos_token_objects::collection;
    use aptos_token_objects::token;

    use aptos_token_objects::property_map;

    ///  To generate resource account
    const STATE_SEED: vector<u8> = b"hero_signer";
    
    const COLLECTION_NAME: vector<u8> = b"HERO COLLECTION";
    const COLLECTION_URI: vector<u8> = b"https://google.com";

    /// error code 
    const ENOT_CREATOR: u64 = 1004;

    /// Global state
    struct State has key {
        // the signer cap of the module's resource account
        signer_cap: SignerCapability
    }


    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    struct Hero has key {
        level: u64,
        mutator_ref: token::MutatorRef, 
        property_mutator_ref: property_map::MutatorRef, 
    }

    fun init_module(account: &signer) {
        let (resource_account, signer_cap) = account::create_resource_account(account, STATE_SEED);
        let collection = string::utf8(COLLECTION_NAME);
        collection::create_unlimited_collection(
            &resource_account,
            string::utf8(COLLECTION_NAME),
            collection,
            option::none(),
            string::utf8(COLLECTION_URI),
        );

        move_to(account, State {
            signer_cap
        });

        // move_to(&resource_account, State {
        //     signer_cap
        // });
    }

    fun create(
        _creator: &signer,
        description: String,
        name: String,
        uri: String,
    ): ConstructorRef acquires State {

        let state = borrow_global_mut<State>(@hero);
        let resource_account = account::create_signer_with_capability(&state.signer_cap); 
        token::create_named_token(
            &resource_account,
            string::utf8(COLLECTION_NAME), 
            description,
            name,
            option::none(),
            uri,
        )
    }

    // Creation methods

    public fun create_hero(
        creator: &signer,
        name: String,
        description: String,
        uri: String,
        level: u64,
    ): Object<Hero> acquires State {

        // generate resource acct
        let state = borrow_global_mut<State>(@hero);
        let resource_account = account::create_signer_with_capability(&state.signer_cap);

        let constructor_ref = create(&resource_account, description, name, uri);
        let token_signer = object::generate_signer(&constructor_ref);

        // <-- create properties
        let property_mutator_ref = property_map::generate_mutator_ref(&constructor_ref); 
        let properties = property_map::prepare_input(vector[], vector[], vector[]);

        property_map::init(&constructor_ref, properties);

        property_map::add_typed<u64>(
            &property_mutator_ref,
            string::utf8(b"level"),
            level,
        );
        // create properties -->

        let hero = Hero {
            level: level,
            mutator_ref: token::generate_mutator_ref(&constructor_ref),
            property_mutator_ref,
        };
        move_to(&token_signer, hero);

        // move to creator

        let transfer_ref = object::generate_transfer_ref(&constructor_ref);
        let creator_address = signer::address_of(creator);
        object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), creator_address);

        object::address_to_object(signer::address_of(&token_signer))
    }

    // Entry functions

    public entry fun mint_hero(
        account: &signer,
        name: String,
        description: String,
        uri: String,
    ) acquires State {
        create_hero(account, name, description, uri, 0);
    }

    public entry fun update_hero_level(
        account: &signer,
        collection: String,
        name: String
        level: u64
    ) acquires Hero, State {
        // ger resource account
        let state = borrow_global_mut<State>(@hero);
        let resource_account = account::create_signer_with_capability(&state.signer_cap);

        // get hero by creator
        let (_hero_obj, hero) = get_hero(
            &signer::address_of(&resource_account),
            &collection,
            &name
        );

        let account_address = signer::address_of(account);
        
        // rule: only owner could modify this one.
        // assert!(object::is_owner(hero_obj, account_address), ENOT_OWNER);
        // rule 0x02: only cap owner could modify this one.
        assert!(account_address == @hero, ENOT_CREATOR);
        // Gets `property_mutator_ref` of hero.
        let property_mutator_ref = &hero.property_mutator_ref;
        // Updates the description property.
        property_map::update_typed(property_mutator_ref, &string::utf8(b"level"), level);
    }


    // View functions

    #[view]
    public fun view_hero(creator: address, collection: String, name: String): Hero acquires Hero {
        let token_address = token::create_token_address(
            &creator,
            &collection,
            &name,
        );
        move_from<Hero>(token_address)
    }

    #[view]
    public fun view_hero_by_object(hero_obj: Object<Hero>): Hero acquires Hero {
        let token_address = object::object_address(&hero_obj);
        move_from<Hero>(token_address)
    }

    inline fun get_hero(creator: &address, collection: &String, name: &String): (Object<Hero>, &Hero) {
        let token_address = token::create_token_address(
            creator,
            collection,
            name,
        );
        (object::address_to_object<Hero>(token_address), borrow_global<Hero>(token_address))
    }
}

可以通过如下链接查看线上部署的合约:

https://explorer.aptoslabs.com/account/0x3fb7233a48d6f0a8c50e1d1861521790af0c5d7cfaf95ec81dc5bed4541becf1/modules/run/hero/mint_hero?network=testnet

以下是在浏览器中进行函数操作:

  1. Mint 一个 Hero。
image-20240320212813440
  1. Mint 到的 Hero 可以在 Token 列表中看到,也可以在钱包中看到。
image-20240320212851838
image-20240320212914887
image-20240320213618912
  1. 升级 hero,这个函数仅能被合约发布人调用。
image-20240320213335033
  1. hero 的等级从 0 级上升为 1 级。
image-20240320213437855

0x01 init_module 函数

fun init_module(account: &signer) {
    let (resource_account, signer_cap) = account::create_resource_account(account, STATE_SEED);
    let collection = string::utf8(COLLECTION_NAME);
    collection::create_unlimited_collection(
        &resource_account,
        string::utf8(COLLECTION_NAME),
        collection,
        option::none(),
        string::utf8(COLLECTION_URI),
    );

    move_to(account, State {
        signer_cap
    });

    // move_to(&resource_account, State {
    //     signer_cap
    // });
}

init_module是合约中的特殊函数,在部署的时候会被一次调用。

在本合约的init_module中,执行了如下功能:

  1. 在发布账户下创建资源账户 resource_ccount
  2. 创建collection
  3. 将资源的账户的签名权限转移到发布账户下

💡如果将现有的move_to替换为注释中的move_to,会有什么改变?

0x02 mint_hero 函数

遵循函数尽量「原子化」的原则,我们将mint_hero函数进行拆分:

mint_hero -> create_hero -> create

2.1 mint_hero

public entry fun mint_hero(
    account: &signer,
    name: String,
    description: String,
    uri: String,
) acquires State {
    create_hero(account, name, description, uri, 0);
}

public entry函数修饰符表示此函数可以被外部调用。

2.2 create_hero

// Creation methods
public fun create_hero(
    creator: &signer,
    name: String,
    description: String,
    uri: String,
    level: u64,
): Object<Hero> acquires State {

    // generate resource acct
    let state = borrow_global_mut<State>(@hero);
    let resource_account = account::create_signer_with_capability(&state.signer_cap);

    let constructor_ref = create(&resource_account, description, name, uri);
    let token_signer = object::generate_signer(&constructor_ref);

    // <-- create properties
    let property_mutator_ref = property_map::generate_mutator_ref(&constructor_ref); 
    let properties = property_map::prepare_input(vector[], vector[], vector[]);

    property_map::init(&constructor_ref, properties);

    property_map::add_typed<u64>(
        &property_mutator_ref,
        string::utf8(b"level"),
        level,
    );
    // create properties -->

    let hero = Hero {
        level: level,
        mutator_ref: token::generate_mutator_ref(&constructor_ref),
        property_mutator_ref,
    };
    move_to(&token_signer, hero);

    // move to creator

    let transfer_ref = object::generate_transfer_ref(&constructor_ref);
    let creator_address = signer::address_of(creator);
    object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), creator_address);

    object::address_to_object(signer::address_of(&token_signer))
}

create_hero 函数里面实现了标准的NFT创建流程。

  1. 从发布者的账户中借出signer_cap,生成资源账户:
// generate resource acct
let state = borrow_global_mut<State>(@hero);
let resource_account = account::create_signer_with_capability(&state.signer_cap);

let constructor_ref = create(&resource_account, description, name, uri);
let token_signer = object::generate_signer(&constructor_ref);
  1. 初始化 properties_map属性表
// <-- create properties
let property_mutator_ref = property_map::generate_mutator_ref(&constructor_ref); 
let properties = property_map::prepare_input(vector[], vector[], vector[]);

property_map::init(&constructor_ref, properties);

property_map::add_typed<u64>(
    &property_mutator_ref,
    string::utf8(b"level"),
    level,
);
// create properties -->
  1. 创建 hero_object 并转移给交易发起人account
let hero = Hero {
    level: level,
    mutator_ref: token::generate_mutator_ref(&constructor_ref),
    property_mutator_ref,
};
move_to(&token_signer, hero);

// move to creator

let transfer_ref = object::generate_transfer_ref(&constructor_ref);
let creator_address = signer::address_of(creator);
object::transfer_with_ref(object::generate_linear_transfer_ref(&transfer_ref), creator_address);

object::address_to_object(signer::address_of(&token_signer))

2.3 create

fun create(
    _creator: &signer,
    description: String,
    name: String,
    uri: String,
): ConstructorRef acquires State {

    let state = borrow_global_mut<State>(@hero);
    let resource_account = account::create_signer_with_capability(&state.signer_cap); 
    token::create_named_token(
        &resource_account,
        string::utf8(COLLECTION_NAME), 
        description,
        name,
        option::none(),
        uri,
    )
}

create处于被调用的最底端,其作用是创建 NFT。

0x03 view 函数

本实例中包含两个view函数:

#[view]
public fun view_hero(creator: address, collection: String, name: String): Hero acquires Hero {
    let token_address = token::create_token_address(
        &creator,
        &collection,
        &name,
    );
    move_from<Hero>(token_address)
}

#[view]
public fun view_hero_by_object(hero_obj: Object<Hero>): Hero acquires Hero {
    let token_address = object::object_address(&hero_obj);
    move_from<Hero>(token_address)
}

这两个函数用以让 dApp 更方便地查看 hero 资料。

0x04 update_hero_level 函数

public entry fun update_hero_level(
    account: &signer,
    collection: String,
    name: String
    level: u64
) acquires Hero, State {
    // ger resource account
    let state = borrow_global_mut<State>(@hero);
    let resource_account = account::create_signer_with_capability(&state.signer_cap);

    // get hero by creator
    let (_hero_obj, hero) = get_hero(
        &signer::address_of(&resource_account),
        &collection,
        &name
    );

    let account_address = signer::address_of(account);

    // rule 0x01: only owner could modify this one.
    // assert!(object::is_owner(hero_obj, account_address), ENOT_OWNER);
    // rule 0x02: only cap owner could modify this one.
    assert!(account_address == @hero, ENOT_CREATOR);
    // gets `property_mutator_ref` of hero.
    let property_mutator_ref = &hero.property_mutator_ref;
    // Updates the description property.
    property_map::update_typed(property_mutator_ref, &string::utf8(b"level"), level);
}

这个函数遵循如下过程:

  1. 拿到资源账户
  2. 通过创建人拿到 NFT
  3. 对 NFT 的属性进行升级

💡如果将 rule 0x02 改成 rule 0x01,会发生什么?


Move 智能合约生产环境升级实践 | Move dApp 极速入门(贰拾陆)
Aptos Cheatsheets | Move dApp 极速入门(贰拾伍)
Aptos 中文区全球线上黑客松!奖池 $12K+
Aptos Token Object V2 | Move dApp 极速入门(贰拾肆)
可編程交易塊 | Move dApp 極速入門(貳拾叁)
Aptos 密鑰輪換 | Move dApp 極速入門(貳拾貳)
Aptos 对象模型 | Move dApp 极速入门(贰拾壹)
Aptos Moveflow SDK 使用指南 | Move dApp 极速入门(贰拾)
Sui 上简单 Swap 的实现 | Move dApp 极速入门(拾玖)
用 Elixir 交互 Aptos |  Move dApp 极速入门(拾捌)
Sui 链上数据查询 | Move dApp 极速入门(拾柒)
SUI 合约测试攻略 | Move dApp 极速入门(拾陆)
Sui 数据类型详解 | Move dApp 极速入门(十五)
Airdropper Contract in Aptos | Move dApp 极速入门(拾肆)
Sandwich 合约案例实践 | Move dApp 极速入门(拾叁)
Sui  | Move dApp 
Move  | 
Move  | 
scaffold-aptos  | Move dApp 
Aptos NFT  | Move dApp 
 DID Document  | Move dApp 
DID | Move dApp 
Aptos  | Move dApp 
Aptos CLI使REPL | Move dApp 
 DID  | Move dApp 
 | Move dApp 
 | Move dApp
 Move dApp | Move dApp
Hello Move | Move dApp

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

Leeduckgo
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开