Fungible Asset

Deploy your Fungible Asset!

앞선 튜토리얼을 통해 Module을 작성하고 배포하는 방법과 트랜잭션을 통해 Module의 함수를 호출하여 Module과 상호작용하는 방법을 알아보았습니다.

이번 튜토리얼에서는 지금까지 배운 내용을 이용하여 Fungible Asset Module을 배포하고 나만의 Fungible Asset을 만들어 보도록 하겠습니다.

📘

이 튜토리얼을 통해 아래 내용을 배울 수 있습니다!

  • Fungible Asset을 생성하는 로직과 방법을 배울 수 있습니다.
  • Fungible Asset Module과 상호작용하여 전송하고 소각할 수 있습니다.
  • 전체 튜토리얼 코드는 아래 링크를 통해 확인할 수 있습니다.

Step 1. Initializing Move environment and Profile

튜토리얼 진행을 위해 새로운 프로젝트 디렉토리를 생성하고 터미널의 경로를 해당 디렉토리로 변경합니다.

$ mkdir FungibleAsset
$ cd FungibleAsset

Move environment를 초기화하기 위해 아래 명령어를 입력합니다.

$ aptos move init --name <your_porject_name>

아래 명령어를 실행하여 초기 계정 정보를 설정합니다.

$ aptos init --network testnet

config.yaml 파일에서 account 값을 복사한 뒤 Move.toml 파일의 [addresses]에 붙여넣어 배포할 Account의 address를 설정합니다. 설정 완료 후 sources 디렉토리 내에 move 파일을 생성합니다.

$ cd sources
$ touch fungible_asset.move

Step 2. Writing Fungible Asset Module

전체적인 Fungible Asset의 코드는 다음과 같습니다. 해당 코드는 Aptos 재단에서 제공하는 기본적인 Fungible Asset 모듈을 일부 수정한 것으로 전체 코드는 여기에서 확인할 수 있습니다.

// fungible_asset.move

module myAddress::fa_coin {
    use aptos_framework::fungible_asset::{Self, MintRef, TransferRef, BurnRef, Metadata, FungibleAsset};
    use aptos_framework::object::{Self, Object};
    use aptos_framework::primary_fungible_store;
    use aptos_framework::function_info;
    use aptos_framework::dispatchable_fungible_asset;
    use std::error;
    use std::signer;
    use std::string::{Self, utf8};
    use std::option;

    /// Only fungible asset metadata owner can make changes.
    const ENOT_OWNER: u64 = 1;
    const ASSET_SYMBOL: vector<u8> = b"MFA";

    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    /// Hold refs to control the minting, transfer and burning of fungible assets.
    struct ManagedFungibleAsset has key {
        mint_ref: MintRef,
        transfer_ref: TransferRef,
        burn_ref: BurnRef,
    }
 

    /// Initialize metadata object and store the refs.
    // :!:>initialize
    fun init_module(admin: &signer) {
        let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL);
        primary_fungible_store::create_primary_store_enabled_fungible_asset(
            constructor_ref,
            option::none(),
            utf8(b"MyFAToken"), /* name */
            utf8(ASSET_SYMBOL), /* symbol */
            8, /* decimals */
            utf8(b"http://example.com/favicon.ico"), /* icon */
            utf8(b"http://example.com"), /* project */
        );

        // Create mint/burn/transfer refs to allow creator to manage the fungible asset.
        let mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
        let burn_ref = fungible_asset::generate_burn_ref(constructor_ref);
        let transfer_ref = fungible_asset::generate_transfer_ref(constructor_ref);
        let metadata_object_signer = object::generate_signer(constructor_ref);
        move_to(
            &metadata_object_signer,
            ManagedFungibleAsset { mint_ref, transfer_ref, burn_ref }
        ); // <:!:initialize

        // Override the deposit and withdraw functions which mean overriding transfer.
        // This ensures all transfer will call withdraw and deposit functions in this module
        // and perform the necessary checks.
        // This is OPTIONAL. It is an advanced feature and we don't NEED a global state to pause the FA coin.
        let deposit = function_info::new_function_info(
            admin,
            string::utf8(b"fa_coin"),
            string::utf8(b"deposit"),
        );
        let withdraw = function_info::new_function_info(
            admin,
            string::utf8(b"fa_coin"),
            string::utf8(b"withdraw"),
        );
        dispatchable_fungible_asset::register_dispatch_functions(
            constructor_ref,
            option::some(withdraw),
            option::some(deposit),
            option::none(),
        );
    }

    #[view]
    /// Return the address of the managed fungible asset that's created when this module is deployed.
    public fun get_metadata(): Object<Metadata> {
        let asset_address = object::create_object_address(&@myAddress, ASSET_SYMBOL);
        object::address_to_object<Metadata>(asset_address)
    }

    /// Deposit function override to ensure that the account is not denylisted and the FA coin is not paused.
    /// OPTIONAL
    public fun deposit<T: key>(
        store: Object<T>,
        fa: FungibleAsset,
        transfer_ref: &TransferRef,
    ) {
        fungible_asset::deposit_with_ref(transfer_ref, store, fa);
    }

    /// Withdraw function override to ensure that the account is not denylisted and the FA coin is not paused.
    /// OPTIONAL
    public fun withdraw<T: key>(
        store: Object<T>,
        amount: u64,
        transfer_ref: &TransferRef,
    ): FungibleAsset  {
        fungible_asset::withdraw_with_ref(transfer_ref, store, amount)
    }

    // :!:>mint
    /// Mint as the owner of metadata object.
    public entry fun mint(admin: &signer, to: address, amount: u64) acquires ManagedFungibleAsset {
        let asset = get_metadata();
        let managed_fungible_asset = authorized_borrow_refs(admin, asset);
        let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
        let fa = fungible_asset::mint(&managed_fungible_asset.mint_ref, amount);
        fungible_asset::deposit_with_ref(&managed_fungible_asset.transfer_ref, to_wallet, fa);
    }// <:!:mint

    /// Transfer as the owner of metadata object ignoring `frozen` field.
    public entry fun transfer(admin: &signer, from: address, to: address, amount: u64) acquires ManagedFungibleAsset {
        let asset = get_metadata();
        let transfer_ref = &authorized_borrow_refs(admin, asset).transfer_ref;
        let from_wallet = primary_fungible_store::primary_store(from, asset);
        let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
        let fa = withdraw(from_wallet, amount, transfer_ref);
        deposit(to_wallet, fa, transfer_ref);
    }
    
    /// Burn fungible assets as the owner of metadata object.
    public entry fun burn(admin: &signer, from: address, amount: u64) acquires ManagedFungibleAsset {
        let asset = get_metadata();
        
        let burn_ref = &authorized_borrow_refs(admin, asset).burn_ref;
        let from_wallet = primary_fungible_store::primary_store(from, asset);
        fungible_asset::burn_from(burn_ref, from_wallet, amount);
    }

    /// Borrow the immutable reference of the refs of `metadata`.
    /// This validates that the signer is the metadata object's owner.
    inline fun authorized_borrow_refs(
        owner: &signer,
        asset: Object<Metadata>,
    ): &ManagedFungibleAsset acquires ManagedFungibleAsset {
        assert!(object::is_owner(asset, signer::address_of(owner)), error::permission_denied(ENOT_OWNER));
        borrow_global<ManagedFungibleAsset>(object::object_address(&asset))
    }
}

최초 Module 배포 시 init_module 함수를 실행하여 Object를 생성하고 Metadata를 Object에게 Resource로 할당합니다. 이후 해당 Metadata Resource를 이용하여 Fungible Asset을 관리할 수 있는 권한(Ref)을 생성한 뒤 Object에게 Resource로 할당합니다.

이러한 방식으로 Module을 작성하면 해당 Object의 Owner는 Mint, Transfer, Burn 등의 함수를 실행할 수 있는 권한을 획득하게 됩니다. 이후 Module에 정의한 함수를 실행하여 Mint, Transfer, Burn 등 원하는 로직을 구현할 수 있습니다.


💡 **Ref는 권한을 관리할 수 있는 강력한 방법 중 하나 입니다!**

본 튜토리얼은 Ref 사용 예시를 위해 Object의 Owner만 Ref를 가질 수 있도록 작성되었습니다. 실제 서비스를 위한 Module 작성 시 본인의 로직에 맞게 설정하여 코드를 작성할 수 있도록 주의해야 합니다.


Step 3. Understanding Module

이번 Step에서는 Module이 어떻게 구성되어 있는지 알아봅니다.


  • struct ManagedFungibleAsset : 권한을 관리하는 Ref를 저장하기 위한 구조체 입니다. 현재 코드에서는 mint, transfer, burn 3가지 기능을 실행하기 위한 권한을 확인하는 Ref를 관리하도록 작성되어 있습니다.
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ManagedFungibleAsset has key {
    mint_ref: MintRef,
    transfer_ref: TransferRef,
    burn_ref: BurnRef,
}

  • 객체 생성 및 초기화
    Module 배포 시 실행되는 constructor 함수 입니다. Object를 생성하고 구조체를 해당 Object에 할당한 후 constructor_ref를 반환합니다. 그리고 constructor_ref와 입력한 인자 값을 이용해 삭제가 불가능한 구조체를 생성하고 Object에 할당합니다.
fun init_module(admin: &signer) {
    let constructor_ref = &object::create_named_object(admin, ASSET_SYMBOL);
    primary_fungible_store::create_primary_store_enabled_fungible_asset(
        constructor_ref,
        option::none(),
        utf8(b"MyFAToken"), /* name */
        utf8(ASSET_SYMBOL), /* symbol */
        8, /* decimals */
        utf8(b"http://example.com/favicon.ico"), /* icon */
        utf8(b"http://nodit.lambda256.io"), /* project */
    );


  • 관리 참조(Ref) 생성
    생성한 constructor_ref를 이용해 mint_ref, burn_ref, transfer_ref을 생성하고 구조체에 입력 후 Object에 할당합니다.
    이를 통해 Mint, Burn, Transfer 기능을 실행할 수 있는 권한을 관리합니다.

    // Create mint/burn/transfer refs to allow creator to manage the fungible asset.
    let mint_ref = fungible_asset::generate_mint_ref(constructor_ref);
    let burn_ref = fungible_asset::generate_burn_ref(constructor_ref);
    let transfer_ref = fungible_asset::generate_transfer_ref(constructor_ref);
    let metadata_object_signer = object::generate_signer(constructor_ref);
    move_to(
        &metadata_object_signer,
        ManagedFungibleAsset { mint_ref, transfer_ref, burn_ref }
    ); // <:!:initialize


  • 함수 구조체 생성 및 할당
    function_info::new_function_info 함수를 이용해 fa_coin 모듈의 deposit 함수와 withdraw 함수에 대한 구조체를 생성하고 이를 Object에 할당합니다. 이를 통해 유저는 deposit, withdraw 함수에 커스텀 로직을 작성하여 실행할 수 있습니다.

    let deposit = function_info::new_function_info(
        admin,
        string::utf8(b"fa_coin"),
        string::utf8(b"deposit"),
    );
    let withdraw = function_info::new_function_info(
        admin,
        string::utf8(b"fa_coin"),
        string::utf8(b"withdraw"),
    );
    dispatchable_fungible_asset::register_dispatch_functions(
        constructor_ref,
        option::some(withdraw),
        option::some(deposit),
        option::none(),
    );
}

  • Object 가져오기
    Account Address와 Seed 값(본 튜토리얼에서는 Symbol 값)을 이용해 Object의 주소를 생성하고 해당 주소와 일치하는 Object를 반환합니다.
public fun get_metadata(): Object<Metadata> {
    let asset_address = object::create_object_address(&@myAddress, ASSET_SYMBOL);
    object::address_to_object<Metadata>(asset_address)
}

  • deposit 함수 정의
    Object와 FungibleAsset 구조체, transfer_ref를 이용해 deposit_with_ref 함수를 실행합니다. 해당 함수의 실행 결과로 Object에 할당된 balance에 amount 만큼 증가시킵니다.
public fun deposit<T: key>(
    store: Object<T>,
    fa: FungibleAsset,
    transfer_ref: &TransferRef,
) {
    fungible_asset::deposit_with_ref(transfer_ref, store, fa);
}


  • withdraw 함수 정의
    Object와 FungibleAsset 구조체, transfer_ref를 이용해 withdraw_with_ref 함수를 실행합니다. 해당 함수의 실행 결과로 Object에 할당된 balance에 amount 만큼 감소시킵니다.
public fun withdraw<T: key>(
    store: Object<T>,
    amount: u64,
    transfer_ref: &TransferRef,
): FungibleAsset  {
    fungible_asset::withdraw_with_ref(transfer_ref, store, amount)
}

  • mint 함수 정의
    Object를 불러온 후 signer와 Object를 이용하여 Mint 권한을 가져옵니다. 자산을 전송받는 주소에 잔액을 확인할 수 있는 구조체 존재 유무 확인 후 없으면 이를 생성합니다. 이후 fungible_asset::mint 함수를 실행하여 구조체를 생성하고 to 주소의 구조체의 잔고를 구조체의 amount 만큼 증가시킵니다.
public entry fun mint(admin: &signer, to: address, amount: u64) acquires ManagedFungibleAsset {
    let asset = get_metadata();
    let managed_fungible_asset = authorized_borrow_refs(admin, asset);
    let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
    let fa = fungible_asset::mint(&managed_fungible_asset.mint_ref, amount);
    fungible_asset::deposit_with_ref(&managed_fungible_asset.transfer_ref, to_wallet, fa);
}

  • transfer 함수 정의
    Object를 불러온 후 signer와 Object를 이용하여 Transfer 권한을 가져옵니다. 자산을 전송하는 주소의 FungibleStore를 확인한 뒤 입력한 amount 만큼 자산을 감소시키고 자산을 전송받는 주소의 FungibleStore의 자산을 확인한 후 amount 만큼 자산을 증가시킵니다.
public entry fun transfer(admin: &signer, from: address, to: address, amount: u64) acquires ManagedFungibleAsset {
    let asset = get_metadata();
    let transfer_ref = &authorized_borrow_refs(admin, asset).transfer_ref;
    let from_wallet = primary_fungible_store::primary_store(from, asset);
    let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, asset);
    let fa = withdraw(from_wallet, amount, transfer_ref);
    deposit(to_wallet, fa, transfer_ref);
}

  • burn 함수 정의
    Object를 불러온 후 signer와 Object를 이용하여 Burn 권한을 가져온 후 from 주소가 보유한 수량에서 amount 만큼 차감합니다.
public entry fun burn(admin: &signer, from: address, amount: u64) acquires ManagedFungibleAsset {
    let asset = get_metadata();
    let burn_ref = &authorized_borrow_refs(admin, asset).burn_ref;
    let from_wallet = primary_fungible_store::primary_store(from, asset);
    fungible_asset::burn_from(burn_ref, from_wallet, amount);
}

  • 검증된 Ref 불러오기
    Owner의 signer 값과 Object를 이용하여 소유자를 검증하고 구조체를 불러옵니다. 이를 바탕으로 Mint, Transfer, Burn 등의 Ref가 필요한 로직에 이용할 수 있습니다.
inline fun authorized_borrow_refs(
    owner: &signer,
    asset: Object<Metadata>,
): &ManagedFungibleAsset acquires ManagedFungibleAsset {
    assert!(object::is_owner(asset, signer::address_of(owner)), error::permission_denied(ENOT_OWNER));
    borrow_global<ManagedFungibleAsset>(object::object_address(&asset))
	}
}

Step 4. Deploy your Module

최종적으로 완성한 Module은 Aptos CLI를 이용해 Aptos 네트워크에 배포할 수 있습니다. 배포 방법은 다음과 같습니다.

  1. Module Compile

move 파일이 작성되어 있는 경로에서 Aptos CLI를 이용해 Module을 컴파일 합니다.

$ aptos move compile

  1. Module Deploy

Compile이 완료된 move 파일을 Aptos CLI를 이용해 Aptos 네트워크에 배포합니다. move 파일이 작성되어 있는 경로에서 아래 명령어를 실행합니다.

$ aptos move publish

Nodit에서 제공하는 Aptos Node API 중 Get account module API를 이용하면 특정 Account가 배포한 Module을 확인할 수 있습니다.


📘

Nodit Aptos Node API는 어떻게 사용할 수 있나요?

아래 링크를 클릭하여 Get account module API를 더욱 자세히 알아보세요!


Get account module API는 Query Params로 Account Address와 moduleName을 이용합니다. header로 X-API-KEY를 추가하고 Nodit을 통해 발급받은 X-API-KEY를 입력한 후 API를 호출해 보세요!

curl --request GET \
     --url https://aptos-testnet.nodit.io/v1/accounts/<account_address>/module/<module_name> \
     --header 'X-API-KEY: <Your X-API-KEY>' \
     --header 'accept: application/json'

아래와 같이 배포한 Module을 확인할 수 있습니다.

{
  "bytecode": "0xa11ceb0b060000...",
  "abi": {
    "address": "0xabc...90",
    "name": "<your_module_name>",
		...
  }
}

Step 5. Interact to Module

Module 배포에 성공했다면 트랜잭션을 통해 Module에 작성된 함수를 호출하여 Fungible Asset을 발행할 수 있습니다. 트랜잭션의 순서는 다음과 같습니다.

  • 트랜잭션 빌드
  • 트랜잭션 시뮬레이션
  • 트랜잭션 서명
  • 트랜잭션 제출
  • 트랜잭션 결과 확인

Aptos Typescript SDK를 이용하여 트랜잭션을 실행해 보세요!

import {
  Account,
  Aptos,
  AptosConfig,
  Ed25519PrivateKey,
} from "@aptos-labs/ts-sdk";

const config = new AptosConfig({
  fullnode: "your_Nodit_Aptos_testnet_node_endpoint",
  indexer: "your_Nodit_Aptos_indexer_endpoint",
});

const aptos = new Aptos(config);

const privateKey = "your_private_key"; // 0x12345...
const ed25519Scheme = new Ed25519PrivateKey(privateKey);
const senderAccount = Account.fromPrivateKey({
  privateKey: ed25519Scheme,
});
const amount: number = 100_000_000_000; // change amount to mint

(async (senderAccount: Account, amount: number) => {
  try {
    const senderAddress = senderAccount.accountAddress.toString();
    const transaction = await aptos.transaction.build.simple({
      sender: senderAddress,
      data: {
        function: "module_owner_address::fungible_asset::mint", //0x1::aptos_account::transfer
        functionArguments: [
          senderAddress,
          amount, // mint function requires to_address and amount as arguments
        ],
      },
    });

    const senderAuthenticator = aptos.transaction.sign({
      signer: senderAccount,
      transaction,
    });

    const submitTx = await aptos.transaction.submit.simple({
      transaction,
      senderAuthenticator,
    });

    const executedTransaction = await aptos.waitForTransaction({
      transactionHash: submitTx.hash,
    });

    console.log(executedTransaction);
  } catch (error) {
    console.error(error);
  }
})(senderAccount, amount);


성공적으로 발행이 되었다면 다른 주소로 전송하는 transfer 함수를 이용해 방금 발행한 Fungible Asset을 전송해 보세요!


위의 코드를 실행하여 Module과 Fungible Asset을 배포하고 생성할 수 있나요?

  • 확인이 되지 않는다면 위의 코드와 다른 부분이 있는지 확인해 보세요.
  • 위의 코드와 차이가 없는데 되지 않나요? 여기[QnA 링크]를 클릭하여 QnA로 남겨주세요!

📘

Aptos는 매우 빠르게 업데이트 되고 있습니다!

Aptos 재단에서 배포한 SDK 버전에 따라 변경되는 점이 있을 수 있습니다. Nodit은 항상 이를 확인하고 있으나 시점에 따라 코드 변경으로 인한 오류가 발생할 수 있습니다.