Mint NFTs with the Aptos CLI
This code lab lets you use the Aptos CLI to mint non-fungible tokens (NFTs) in Aptos so you can see how the process works and employ related functions in your code. Although this code lab assumes use of the devnet network, you could follow the same steps in devnet or even mainnet, assuming you have the necessary funds in your account.
Prerequisites
This tutorial assumes you have:
- a GitHub account
- the GitHub CLI
- the Aptos CLI (or you can run from aptos-core source via
cargo run
) - the
aptos-core
repository checked out:git clone https://github.com/aptos-labs/aptos-core.git
Then:
- Review the mint_nft source code and code comments within each subdirectory.
- Explore the
mint_event_ticket
function defined within each subdirectory.
NFT types
The two most common types of NFT are event ticket / certificates and profile picture NFTs.
Event tickets and certificates
This kind of NFT has a base token, and every new NFT generated from this base token has the same token data ID and image. They are generally used as certificates, meaning each NFT created from the base token is considered a printing edition of the base token.
You might use this type of NFT for event tickets where each NFT is a ticket and has properties representing expiration date and if the ticket has been used. When you mint the NFT, you may set an expiration time for the event ticket and is_ticket_used
to false
. When the ticket is used, you update is_ticket_used
to true
.
Profile picture NFTs
A profile picture NFT has a unique token data ID and picture for each token. There are generally no printing editions of this NFT. Most NFT collections on NFT marketplaces are of this kind. They are generally proofs of ownership of an art piece.
In this tutorial, we describe how to create and mint event ticket NFTs.
1. Create a collection and token
Now that you have an understanding of the Aptos minting process and are starting to write smart contracts with Move, you are ready to create your first devnet NFT with the Aptos CLI.
In this section, we create a collection and token. This work maps to the demonstration in create_nft.move.
Create an account
Create a default (typical) account on Aptos devnet to receive the NFT by running the following command and selecting
devnet
:aptos init
Receive the output:
Configuring for profile default
When prompted for a network:
Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet]
Select
devnet
by entering it and hitting return.When prompted for your private key:
Enter your private key as a hex literal (0x...) [Current: None | No input: Generate new key (or keep one if present)]
Hit enter to generate a new key.
Receive output indicating success and resembling:
No key given, generating key...
Account a233bf7be2b93f1e532f8ea88c49e0c70a873d082890b6d9685f89b5e40d50c2 does not exist, you will need to create and fund the account through a community faucet e.g. https://aptoslabs.com/testnet-faucet, or by transferring funds from another account
---
Aptos CLI is now set up for account a233bf7be2b93f1e532f8ea88c49e0c70a873d082890b6d9685f89b5e40d50c2 as profile default! Run `aptos --help` for more information about commands
{
"Result": "Success"
}Note your configuration information can be found in
.aptos/config.yaml
relative to where you ranaptos init
. Read that file to see each profile's private and public keys, account address, and REST API URL.
Publish the module
- In your
aptos-core
source checkout, navigate to theaptos-core/aptos-move/move-examples/mint_nft/1-Create-NFT
directory:
cd aptos-move/move-examples/mint_nft/1-Create-NFT
- Run
aptos move publish
to publish the Move module in that directory, updating it with your default account address from.aptos/config.yaml
:
aptos move publish --named-addresses mint_nft=<default-account-address>
For example:
aptos move publish --named-addresses mint_nft=a911e7374107ad434bbc5369289cf5855c3b1a2938a6bfce0776c1d296271cde
- Receive output asking you to accept a gas fee for the transaction:
Compiling, may take a little while to download git dependencies...
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY AptosToken
INCLUDING DEPENDENCY MoveStdlib
BUILDING Examples
package size 2770 bytes
Do you want to submit a transaction for a range of [1164400 - 1746600] Octas at a gas unit price of 100 Octas? [yes/no]
- Enter
yes
and receive results indicating success:
{
"Result": {
"transaction_hash": "0x576a2e9481e71b629335b98ea75c87d124e1b435e843e7a2ef8938ae21bebfa3",
"gas_used": 11679,
"gas_unit_price": 100,
"sender": "a911e7374107ad434bbc5369289cf5855c3b1a2938a6bfce0776c1d296271cde",
"sequence_number": 0,
"success": true,
"timestamp_us": 1669659103283876,
"version": 12735152,
"vm_status": "Executed successfully"
}
}
See the module on Aptos Explorer
- Go to the Aptos Explorer in a web browser>
- Select the network you used: devnet
- Search for the transaction by
transaction_hash
in the search field, using your own unique transaction hash. - View the changes made in publishing this module under the Changes tab.
Mint a token to a receiver
See create_nft.move to see how to mint a token to a receiver.
2. Use resource account for automation
This part maps to the demonstration in create_nft_with_resource_account.move and introduces the Aptos concept of resource accounts.
Since the Move model often requires knowing the signer of a transaction, Aptos provides resource accounts for assigning signer capability. Creating resource accounts enables access to the signer capability for automated use. With this ability, resource accounts can publish Move modules and automatically sign for transactions and minting. The signer capability can be assigned to the resource account or placed in storage locally within the module.
When you create a resource account you also grant that account the signer capability. If we don't use a resource account for this module, we would need to manually sign for all transactions. The only field inside the signer capability is the address
of the signer.
A native function converts the signer capability to the signer.
let resource_signer = account::create_signer_with_capability(&module_data.signer_cap);
This function creates the signer from the signer capability. See the corresponding entry in create_nft_with_resource_account.move
to see how we create a signer from the signer capability.
Create a resource account
Just as you created a default account on Aptos devnet, you will now create another user account to receive the NFT. To create that account, once again issue aptos init
but this time use the --profile
argument to create a specific account, in this case nft-receiver
.
Create an
nft-receiver
account on Aptos devnet by running the following command.aptos init --profile nft-receiver
Receive the output:
Configuring for profile nft-receiver
Follow the same steps as for the default account to select the
devnet
network and generate a new private key.
Publish the module under the resource account
- Assuming you are in your
aptos-core
source directory, navigate to theaptos-core/aptos-move/move-examples/mint_nft/2-Using-Resource-Account
directory:
cd aptos-move/move-examples/mint_nft/2-Using-Resource-Account
- Run the following CLI command to publish the module under a resource account, including your
<default-account-address>
and a<seed>
of your choosing:
aptos move create-resource-account-and-publish-package --seed <seed> --address-name mint_nft --profile default --named-addresses source_addr=<default-account-address>
Remember, you can find this address in .aptos/config.yaml
. For example:
aptos move create-resource-account-and-publish-package --seed 1235 --address-name mint_nft --profile default --named-addresses source_addr=a911e7374107ad434bbc5369289cf5855c3b1a2938a6bfce0776c1d296271cde
As when you ran
aptos move publish
to mint the NFT, again receive output asking you to accept a gas fee for the transaction and enter:yes
Receive the output:
"Result": "Success"`
Storing signer capability
See create_nft_with_resource_account.move to better understand the related functions:
init_module
- stores the resource account's signer capability withinModuleData
for later usage.mint_event_ticket
- creates a resource signer by callingaccount::create_signer_with_capability(&module_data.signer_cap)
to programmatically sign fortoken::mint_token()
andtoken::direct_transfer()
functions.
Mint an NFT to the nft-receiver account
- Run the following command to mint an NFT to the
nft-receiver
account, replacing<resource-account-address>
with the relevant address from.aptos/config.yaml
:
aptos move run --function-id <resource-account-address>::create_nft_with_resource_account::mint_event_ticket --profile nft-receiver
As before, receive output asking you to accept a gas fee for the transaction and enter:
yes
.Receive output containing:
"vm_status": "Executed successfully"
- See the module on Aptos Explorer as before by searching for its
transaction_hash
.
3. Add admin account and functions
Publishing a module under a resource account means that we will not be able to update the module, and the module will be immutable and autonomous. We need administrator accounts, because we give up control over the smart contracts when we deploy a module using a resource account. Admin accounts and functions let you update the configuration going forward.
If you will need to update the configuration of the module, we suggest adding a few admin functions to control the configurations of the module. You will also need to add the admin account's address to the module's Move.toml
file so the admin functions can confirm the caller to the admin functions is actually a valid administrator.
This work maps to the demonstration in reate_nft_with_resource_and_admin_accounts.move.
Create admin account
Here we create an admin account and include two admin functions so we can set and update when/if we want to enable token minting for this contract.
Run aptos-init
once again, this time to create an admin account:
`aptos init --profile admin`
Publish the module under a resource account
- In your
aptos-core
source checkout, navigate to theaptos-core/aptos-move/move-examples/mint_nft/3-Adding-Admin
directory:
cd aptos-move/move-examples/mint_nft/3-Adding-Admin
Edit
Move.toml
in that directory to replaceadmin_addr = "0xcafe"
with the newadmin
address from.aptos/config.yaml
.Run the following CLI command to publish the module under a resource account, updating the
<seed>
anddefault-account-address>
with your own:
aptos move create-resource-account-and-publish-package --seed <seed> --address-name mint_nft --profile default --named-addresses source_addr=<default-account-address>
As usual, receive output asking you to accept a gas fee for the transaction and enter:
yes
.Receive the output:
"Result": "Success"`
Include admin functions
See create_nft_with_resource_and_admin_accounts.move to understand the related admind functions used:
- In struct
ModuleData
, we add two additional fields:expiration_timestamp
andminting_enabled
. These allow us to set and update when this collection will expire and enable or disable minting ad-hoc, respectively. - We included two admin functions -
set_minting_enabled()
andset_timestamp()
- to update theexpiration_timestamp
andminting_enabled
fields. - In the admin functions, we check if the caller is calling from the valid administrator's address. If not, we abort because the caller does not have permission to update the config of this module.
- In
mint_event_ticket()
, we add twoassert
statements to make sure the user can mint tokens from this collection only if minting is enabled and the collection has not expired.
Mint an NFT to the nft-receiver account
- Run the following command to attempt to mint an NFT, including your
<resource-account-address>
(failure expected):
aptos move run --function-id <resource-account-address>::create_nft_with_resource_and_admin_accounts::mint_event_ticket --profile nft-receiver
- Receive output indicating a failure:
"Error": "Simulation failed with status: Move abort in <resource-account-address>::create_nft_with_resource_and_admin_accounts: EMINTING_DISABLED(0x50003): The collection minting is disabled"
Running this command fails because minting is disabled in init_module()
.
- Use the admin account to update the flag
minting_enabled
to true:
aptos move run --function-id <resource-account-address>::create_nft_with_resource_and_admin_accounts::set_minting_enabled --args bool:true --profile admin
As usual, receive output asking you to accept a gas fee for the transaction and enter:
yes
.Receive output containing:
"vm_status": "Executed successfully"
- Mint the NFT with success:
aptos move run --function-id <resource-account-address>::create_nft_with_resource_and_admin_accounts::mint_event_ticket --profile nft-receiver
Accept the gas fee for the transaction by entering:
yes
.Receive output contaning:
"vm_status": "Executed successfully"
- See the transactions on Aptos Explorer as before by searching for their
transaction_hash
strings.
4. Prepare module for production
The final phase of development is to make the smart contract module ready for production. This work maps to the demonstration in create_nft_getting_production_ready.move.
We prepare the module for production by:
- adding a
TokenMintingEvent
to emit a custom event for tracking minting of tokens from this module. - enabling signature verification and ntroducing the concept of a proof challenge to prevent bot spamming.
- including unit tests to make sure our code works as expected.
Publish the module under a resource account
- In your
aptos-core
source checkout, navigate to theaptos-core/aptos-move/move-examples/mint_nft/1-Create-NFT
directory:
cd aptos-move/move-examples/mint_nft/4-Getting-Production-Ready
Edit
Move.toml
in that directory to replaceadmin_addr = "0xbeef"
with the admin address we created in 3. Add admin account and functions.Run the following CLI command to publish the module under a resource account, replacing
<seed>
and<default-account-address>
with your own:
aptos move create-resource-account-and-publish-package --seed <seed> --address-name mint_nft --profile default --named-addresses source_addr=<default-account-address>
- As usual, receive output asking you to accept a gas fee for the transaction, enter
yes
, and receive results indicating success.
Mint a token with mint_event_ticket()
- Generate a key pair to validate the signature against:
aptos key generate --key-type ed25519 --output-file output.key
- View and copy the public key we just generated:
more output.key.pub
- Update the public key stored within
ModuleData
by running the command and replacing<resource-account-address>
and<new-public-key>
with your own:
aptos move run --function-id <resource-account-address>::create_nft_getting_production_ready::set_public_key --args hex:<new-public-key> --profile admin
- Accept the gas fee for the transaction and receive results indicating success.
Generate a valid signature
- Find and copy the
admin_private_key
:
more output.key
- Find and copy the addresses for the
nft-receiver
andresource
account from.aptos/config.yaml
.
Find and copy the
receiver_account_sequence_number
by looking up the NFT receiver's address on the Aptos Explorer Info tab.Open the file
aptos-core/aptos/move-e2e-tests/src/tests/mint_nft.rs
for editing.In function
generate_nft_tutorial_part4_signature
, replace theresource_address
,nft_receiver
,admin_private_key
, andreceiver_account_sequence_number
variables with the actual values from your temporary copies above.Run to generate a valid signature and copy it:
cargo test generate_nft_tutorial_part4_signature -- --nocapture
- Call mint_event_ticket() with your
<resource-account-address>
and the<new-signature>
we just generated:
aptos move run --function-id <resource-account-address>::create_nft_getting_production_ready::mint_event_ticket --args hex:<new-signature> --profile nft-receiver
- Accept the gas fee for the transaction and receive results indicating success.
This completes the Mint NFT via CLI code lab. Congratulations on your completion.
5. Mint with admin and source accounts
Here is an alternative flow using admin and source accounts.
Mint the NFT
Mint the NFT by calling the
mint_nft
function and an existing contract using the Aptos CLI:aptos move run --function-id 8cdf69c8c93fee36ed83f8882908060c1335ed39a827c08dbb506b46237e88fb::minting::mint_nft --profile nft-receiver
When asked,
Do you want to submit a transaction for a range of...?
, enteryes
and hit return.Receive results resembling:
{
"Result": {
"transaction_hash": "0x6e022532fb8d802324829d5ec85fd32c05a58a6f826751f63cdbf9bf313ff991",
"gas_used": 3944,
"gas_unit_price": 150,
"sender": "b9d394a7bc582a54e8610d6a7b973f62c8d9595c54c35cdbb95965aa8e5cd111",
"sequence_number": 0,
"success": true,
"timestamp_us": 1670969779029341,
"version": 385901038,
"vm_status": "Executed successfully"
}
}
*/
Find the NFT in your Petra wallet
Open the Petra Wallet Chrome extension.
Go to Petra > Settings > Network > Devnet.
Click Library at bottom.
See the NFT in your wallet.
Deploy the NFT contract
Now you can add this smart contract to the Aptos network.
Create and fund admin and source account
Create two accounts on devnet for deploying and managing this contract:
- The source account will be used to create the resource account that will deploy this smart contract.
- The admin account is in charge of updating the config of the module (e.g. whether or not we enable minting for this collection).
Run these commands to create the accounts, selecting
devnet
when prompted:aptos init --profile source-account
aptos init --profile admin-account
Open
.aptos/config.yaml
to find the private keys for theadmin-account
andsource-account
profiles and copy them.Fund these accounts by adding them to your wallet via importing their private keys into devnet and using the Faucet function as you did for the
nft-receiver
profile.
Prepare resource account from source account
In this section, we will create a resource account from the source-account
and publish the module on devnet under the resource account’s address. A resource account is used here to programmatically signed for transactions and avoids the need for multiple signatures.
In the NFT Tutorial smart contract, we store the resource account’s signer capability in the ModuleData
resource so that it can automatically sign for transactions in the contract. If we don’t store the signer capability within the module, we’d need to provide the resource account’s signer; but we no longer have access to the resource account’s signer because the resource account is meant to be autonomous, and the contracts published under a resource account are immutable. Once the contract is published, the resource account no longer has access to the signer.
Clone the NFT Tutorial:
git clone https://github.com/aptos-labs/nft-tutorial
Navigate into the tutorial directory:
cd nft-tutorial
Open
Move.toml
in that directory for editing.Update the
source_addr
andadmin_addr
with theaccount
values for thesource-account
andadmin-account
profiles you just created (found in.aptos/config.yaml
), respectively. Note, the order of addresses and accounts is flipped in those files, so copy carefully.
Create resource account and publish package
Run the following command to create the resource account and publish the package all at once. The seed is just an example - feel free to provide a different seed if the resource account created by the specified seed already exists:
aptos move create-resource-account-and-publish-package --seed hex_array:1234 --address-name mint_nft --profile source-account
Receive compilation output like:
Compiling, may take a little while to download git dependencies...
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY AptosToken
INCLUDING DEPENDENCY MoveStdlib
BUILDING NFT-tutorialWhen prompted to publish this package under the resource account's address, enter
yes
and hit return.When asked,
Do you want to submit a transaction for a range of...?
, enteryes
and hit return.Receive output indicating success resembling:
{
"Result": "Success"
}
Mint more NFTs and disable minting from admin account
- Mint another NFT using the
nft-receiver
profile, sustituting in the resource account's address:
aptos move run --function-id <resource-account-address>::minting::mint_nft --profile nft-receiver
Again answer
yes
when prompted to submit the transaction.Receive output resembling:
{
"Result": {
"transaction_hash": "0x62660973b1a94e620c863899a157b0b46c02dcfdb0c9261a34ed4d2391550fc7",
"gas_used": 6691,
"gas_unit_price": 100,
"sender": "aaf015db7b6dacb1db4637ef017e68e689a40797fe8fd3897cee08808979bb91",
"sequence_number": 0,
"success": true,
"timestamp_us": 1667434233137811,
"version": 27685944,
"vm_status": "Executed successfully"
}
}
*/
- Disable NFT minting in this module by applying
set_minting_enabled --args bool:false
to theadmin-account
profile so that random folks cannot mint this NFT from your module:
aptos move run --function-id <resource-account-address>::minting::set_minting_enabled --args bool:false --profile admin-account
Now you can include your own artwork once you are ready to customize your NFTs by replacing our defaults in minting.move
:
- https://slwdaeeko5tz5hx46c6zwqhmh3c6je4sbdbjsdjzbntme5dxarxa.arweave.net/kuwwEIp3Z56e_PC9m0DsPsXkk5IIwpkNOQtmwnR3BG4
- https://lty5vdw4cl6yczbpz2rnm2732rbtnk3jeiutyqd644wojmkyt2hq.arweave.net/XPHajtwS_YFkL86i1mv71EM2q2kiKTxAfucs5LFYno8
NFT Airdrops
The earlier examples demonstrate how users directly mint NFTs. Aptos also supports airdropping via a two step process:
- The creator mints the token into their
TokenStore
- The creator then offers the token to a receiving account.
- The receiver claims the token.
Using the NFT mint example replace the mint
function with an airdrop
function as below.
public entry fun airdrop(whitelist: vector<address>) acquires ModuleData {
let module_data = borrow_global_mut<ModuleData>(@mint_nft);
let count = vector::length(&whitelists);
let resource_signer = account::create_signer_with_capability(&module_data.signer_cap);
let token_id = token::mint_token(&resource_signer, module_data.token_data_id, count);
let i: u64 = 0;
while(i < count) {
let receiver = vector::pop_back(&mut whitelist);
token_transfers::offer(&resource_signer, receiver, token_id, 1);
i = i + 1;
};
}
After offering the token, the wallet receiver (eg: Petra) would see the offer as shown below: