How to publish and mint NFT on Aptos

|
by: imcoding.online

In this page, we will show you how to create a NFT collection from a resource account and allow users to mint with APTOS network.

You can skip the post and found the full code here.

write the module

First, let's implement a module to create NFT collection and allow minting. We named it IMCODING NFT.

Create a Move package,it contains a Move.toml package manifest file and the directory stored module source file named sources.

imcoding_nft
├── Move.toml
├── sources
    ├── imcoding_nft.move

Move.toml

[package]
name = "IMCODING NFT"
version = "0.0.1"

[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" }
AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token", rev = "main" }

[addresses]
imcoding_nft = "_"
deployer = "_"

In the manifest file, we import two dependencies: aptos_framework and aptos_token. And we declare two named addresses, deployer is the original account which create a resource account to publish the module, imcoding_nftis the resource account.

create the NFT collection

When we pulish the package, the function init_module() will be called. So we create the NFT collection in init_module. We use aptos_token::token module to create NFT collection.

// file: imcoding_nft.move

module imcoding_nft::minting {
  use std::string::{Self, String};
  use std::vector;

  use aptos_token::token::{Self, TokenDataId};
  use aptos_framework::resource_account;
  use aptos_framework::account;

  struct NFTMinter has key {
    signer_cap: account::SignerCapability,
    collection: String,
  }

  fun init_module(resource_account: &signer) {
    // the collection name
    let collection_name = string::utf8(b"IMCODING NFT Collection");
    // the collection description
    let description = string::utf8(b"NFT issued by imcoding.online");
    // the collection properity uri
    let collection_uri = string::utf8(b"https://imcoding.online/properity/collection.svg");

    // defined as 0 if there is no limit to the supply
    let maximum_supply = 1024;
    // https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-token/sources/token.move#L263
    let mutate_setting = vector<bool>[ false, false, false ];

    let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_account, @deployer);
    let resource_signer = account::create_signer_with_capability(&resource_signer_cap);

    token::create_collection(&resource_signer, collection_name, description, collection_uri, maximum_supply, mutate_setting);

    move_to(resource_account, NFTMinter {
      signer_cap: resource_signer_cap,
      collection: collection_name,
    });
  }
}

In the last few lines ot the init_module function, we retrieve the resource account created by the deployer, and use the resource account to create the NFT collection. Then we publish a NFTMinter resource which stored the signer capability under the resource account.

allow users to mint

Let's provide a function to allow users to mint NFT.

// file: imcoding_nft.move

//...
use std::signer::{address_of};
//...

public entry fun mint_nft(receiver: &signer) acquires NFTMinter {
    let receiver_addr = address_of(receiver);

    let nft_minter = borrow_global_mut<NFTMinter>(@imcoding_nft);

    let resource_signer = account::create_signer_with_capability(&nft_minter.signer_cap);
    let resource_account_address = address_of(&resource_signer);

    let token_name = string::utf8(b"IMCODING NFT");
    let token_description = string::utf8(b"");
    let token_uri = string::utf8(b"https://imcoding.online/properity/nft.svg");

    let token_data_id = token::create_tokendata(
      &resource_signer,
      nft_minter.collection,
      token_name,
      token_description,
      1,
      token_uri,
      resource_account_address,
      1,
      0,
      token::create_token_mutability_config(
        &vector<bool>[ false, false, false, false, true ]
      ),
      vector::empty<String>(),
      vector::empty<vector<u8>>(),
      vector::empty<String>(),
    );

    let token_id = token::mint_token(&resource_signer, token_data_id, 1);
    token::direct_transfer(&resource_signer, receiver, token_id, 1);
  }

The function use the signer capability stored in the NFTMinter resource to mint a token and direct transfer to the receiver.

publish the module

We should use create_resource_account_and_publish_package() function defined in resource_account module to create resource account and publish pakcage under the resource account.

Let's create a rust project to build the Move package and invoke the function to publish the package.

$ cargo new deploy

Run the above command, then we have a directory with the following file structure:

deploy
├── src
    ├── main.rs
├── .gitignore
├── Cargo.toml

Add dependencies in the Cargo.toml file.

[package]
name = "deploy"
version = "0.1.0"
edition = "2021"

[dependencies]
aptos-sdk = { git = "https://github.com/aptos-labs/aptos-core", branch = "mainnet" }
framework = { git = "https://github.com/aptos-labs/aptos-core", branch = "mainnet" }
cached-packages = { git = "https://github.com/aptos-labs/aptos-core", branch = "mainnet" }
hex = "0.4.3"
url = "2.3.1"
anyhow = "1.0"
tokio = { version = "1", features = ["full"] }

In the main.rs file, we first create a resource account:

let key_bytes =
        hex::decode(env::var("DEPLOYER_PRIVATE_KEY").unwrap()).unwrap();
let private_key: Ed25519PrivateKey = (&key_bytes[..]).try_into().unwrap();

let address: AccountAddress = env::var("DEPLOYER_ADDRESS").unwrap().parse().unwrap();
let mut account = LocalAccount::new(address.into(), private_key, 0);
let seed:Vec<u8> = Vec::new();
let resource_address = create_resource_address(account.address(), &seed);

println!("resource account: {:?}", resource_address.to_string());

We read the private key and address of the deployer account from environment and create a resource account of the ddeployeraccount with a seed of type Vec<u8>.

Then we build the Move package and get the bytecode and metadata of the built package.

let mut build_options = BuildOptions::default();
build_options.named_addresses.
  insert("imcoding_nft".to_string(), resource_address);
build_options.named_addresses.
  insert("deployer".to_string(), address);

let package = BuiltPackage::build(
  PathBuf::from("../"),
  build_options,
).expect("building package must succeed");

let code = package.extract_code();
let metadata = package.extract_metadata().
  expect("extracting package metadata must succeed");

At last, we generate a create_resource_account_and_publish_package transaction and publish it to the blockchain.

let payload = aptos_stdlib::resource_account_create_resource_account_and_publish_package(
  seed, 
  bcs::to_bytes(&metadata).expect("package metadata has BCS"), 
  code,
);

let base_url = env::var("APTOS_NODE_URL").unwrap_or(String::from("http://127.0.0.1:8080"));
let rest_url = Url::parse(&base_url).expect("url must valid");

let client = rest_client::Client::new(rest_url).clone();

let chain_id = client.get_index()
  .await
  .context("Failed to get chain ID")?
  .inner()
  .chain_id;

let expiration = SystemTime::now().
  duration_since(UNIX_EPOCH).unwrap().as_secs() + 10;


let tx = TransactionBuilder::new(
  payload, expiration, ChainId::new(chain_id),
).sender(address).sequence_number(account.sequence_number() + 1).
  max_gas_amount(2_00_000);

let signed_tx = account.borrow_mut().sign_with_transaction_builder(tx);
let pending_tx = client.submit(&signed_tx).await.context("failed to submit transfer transaction")?.into_inner();
println!("tx: {:?}!", pending_tx.hash.to_string());
client.wait_for_transaction(&pending_tx).await.context("failed when waiting for the transaction")?;

Let's run the deploy script:

$ export DEPLOYER_ADDRESS=59d8469fa18f5613f72f1b2daa2b9227b2a7229c55b102cfcf86e685b38d9515
$ export DEPLOYER_PRIVATE_KEY=b05f9b88c0a50e56a54df017aaa5e864e232d1d4eba99210f62331c8d99440ce
$ export APTOS_NODE_URL=http://127.0.0.1:8080
$ cargo run

If all goes well, the console will output the resource account and transaction hash.

resource account: "e678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9"
tx: "0x1233ab41de4c5b8346b7110b3f163ab835c24265bd93c7e2c99cd0b287af9ed2"!

View this transaction in the blockchain explorer. In the Events tab, we can find a 0x3::token::CreateCollectionEvent which has the data:

{
  "collection_name": "IMCODING NFT Collection",
  "creator": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
  "description": "NFT issued by imcoding.online",
  "maximum": "1024",
  "uri": "https://imcoding.online/properity/collection.svg"
}

mint nft

Invoke the mint_nft function in module minting which publish under the resource account 0xe678...da55a9 to mint the NFT.

$ aptos move run --function-id 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting::mint_nft

After successful execution, the transaction will contain an event named 0x3::token::CreateTokenDataEvent. The event data is roughly as follows:

{
  "description": "",
  "id": {
    "collection": "IMCODING NFT Collection",
    "creator": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
    "name": "IMCODING NFT"
  },
  "maximum": "1",
  "mutability_config": {
    "description": false,
    "maximum": false,
    "properties": true,
    "royalty": false,
    "uri": false
  },
  "name": "IMCODING NFT",
  "property_keys": [],
  "property_types": [],
  "property_values": [],
  "royalty_payee_address": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
  "royalty_points_denominator": "1",
  "royalty_points_numerator": "0",
  "uri": "https://imcoding.online/properity/nft.svg"
}

So far, we have successfully publish an NFT minting module and mint an NFT as a user. The full code can be found here.