NFTs on Aptos
A summary of NFTs on Aptos with links to key resources
Overview
NFTs are unique (non-fungible) tokens that represent art, in-game items, tickets, or other items that someone can own, buy, sell, or trade. Each NFT has a unique identifier, or object id, that distinguishes it from other objects on the Aptos blockchain. On Aptos, an NFT can be owned by an account or another object. An example of an object owning an NFT might be a NFT representing an in-game character owning a NFT representing a sword. NFTs on Aptos can be soul-bound, meaning that once transferred to their owner they cannot be transferred again.
Read below to see an overview of NFTs on Aptos and links to relevant tutorials, standards, etc. We cover the current standard for NFTs on Aptos, the Digital Asset (DA) Standard, which has been live since 2023. We do not cover the previous standard, the Aptos Token Standard, which you can read about here if needed.
Collections, mints, and updates
A no(-Move)-code option
Aptos provides a solution that handles core NFT collection and token requirements at 0x4::AptosTokenObjects
on Mainnet and Testnet (see also the reference doc). You so that you can create collection and mint tokens using pre-published Move code. There are even helper functions in the TypeScript SDK that build the transactions you need (which we use below)!
If you're creating the next viral PFP collection, this may be all that you need. If you're making something more complicated - like the next great adventure game - you may want to write your own Move code that uses some of the 0x4::AptosTokenObjects
functions but adds more logic. We show some examples of this below, but if you want to see more complex examples, make sure to check out the resources at the bottom of the page.
Collections
Every NFT token belong to a collection. So, first you must create a collection with key details like a name, description, maximum supply, and more. Collections and tokens (NFTs) are separate objects with their own distinct addresses.
You can create a Collection
with a fixed supply or an unlimited one. Below is code for creating a collection with a fixed supply and a 1% royalty. For a game, it wouldn’t make much sense to limit the core NFTs players need to play the game - like an initial hero character or a basic sword - as that would limit the number of players you can have. However, you might place limits on certain rare items like an all-powerful sword in order to maintain their specialness - whether by setting a maximum number, a low likelihood of minting, or other limitations.
TypeScript example
You can create one using the Shinami TypeScript SDK (or the Aptos one if you're not using Shinami 😢):
const createCollectionTransaction = await nodeClient.createCollectionTransaction({
creator: collectionCreator,
description: "Items for the best game in the world",
name: "Epic Figtherz Battlez Collection",
uri: "https://link-to-my-game-website",
maxSupply: 1000,
mutableDescription: false,
mutableRoyalty: false,
mutableURI: true,
mutableTokenDescription: true,
mutableTokenName: true,
mutableTokenProperties: true,
mutableTokenURI: true,
tokensBurnableByCreator: false,
tokensFreezableByCreator: false,
royaltyNumerator: 1,
royaltyDenominator: 100
});
await nodeClient.signAndSubmitTransaction({
signer: collectionCreator,
transaction: createCollectionTransaction
});
Here's my create collection transaction on chain and here are the collection's resources. One of the things you can see in the explorer, as shown below, is the fact that I get 1% in royalties when one of my NFTs is sold:
Move example
You can also write your own Move code that calls functions from the 0x4::AptosTokenObjects
package, as shown in the DA Standard doc. Here, we have a supply cap of 1,000 but no royalty:
use aptos_token_objects::collection;
use std::option::{Self, Option};
public entry fun create_collection(creator: &signer) {
let max_supply = 1000;
let royalty = option::none();
// Maximum supply cannot be changed after collection creation
collection::create_fixed_collection(
creator,
"Items for the best game in the world",
max_supply,
"Epic Figtherz Battlez Collection",
royalty,
"https://link-to-my-game-website",
);
}
Then, you'd publish that package and construct a transaction to call the function.
Minting
A mint is a Move function call to the NFT smart contract. In the contract, you define the rules for which addresses can mint and what they can mint. The 0x4::AptosTokenObjects
package mints into the creator's wallet address, and from there the creator can transfer them to another wallet or list them on a marketplace if desired.
TypeScript example
const mintTransaction = await nodeClient.mintDigitalAssetTransaction({
creator: collectionCreator,
collection: "Epic Figtherz Battlez Collection",
description: "A basic sword to help in battle.",
name: "basic sword",
uri: "https://increasing-indigo-platypus.myfilebase.com/ipfs/QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV",
propertyKeys: ["power", "durability"],
propertyTypes: ["U16", "U8"],
propertyValues: [80, 100]
});
const mintTxResp = await nodeClient.signAndSubmitTransaction({
signer: collectionCreator,
transaction: mintTransaction
});
Here's my mint transaction which added a "basic sword" to the tokens I own:
And if you look at the token, you can see the two properties I added (power
and durability
). You can also see that 1 out of a maximum supply of 1,000 has been minted.
Move example
Again, here is the same operation in Move:
use aptos_token_objects::token;
use std::option::{Self, Option};
public entry fun mint_token(creator: &signer) {
let royalty = option::none();
token::create_named_token(
creator,
"Epic Figtherz Battlez Collection",
"A basic sword to help in battle.",
"basic sword",
royalty,
"https://increasing-indigo-platypus.myfilebase.com/ipfs/QmT6RCAd8DJntUT7HGSHYvKSAniSXmMU37hUSRycREj4MV",
);
}
Modify an NFT
A static PFP is nice. But when NFTs can change and add attributes over time, that makes things a lot more interesting. For instance, a sword can lose durability after a fight.
Modify an attribute
Typescript example
Here, we're lowering a sword's durability after being in a battle.
// 1. Check the sword's current durability
const SWORD_ADDRESS = "0xad0709ae89c0bc8e9fbf6fcfbaad9a3fd105384e0870f57e05f3ab0b4371b20a";
const swordData = await nodeClient.getDigitalAssetData({
digitalAssetAddress: SWORD_ADDRESS
});
const FIGHT_DURABILITY_LOSS = 5;
let currentDurability = swordData.token_properties[DURABILITY_NAME];
let newDurability = currentDurability - FIGHT_DURABILITY_LOSS;
console.log("Lowering durability from ", currentDurability, "to ", FIGHT_DURABILITY_LOSS);
// 2. Create and submit a transaction to modify it
const modifyTx = await nodeClient.updateDigitalAssetPropertyTransaction({
creator: collectionCreator,
propertyKey: DURABILITY_NAME,
propertyType: DURABILITY_TYPE,
propertyValue: newDurability,
digitalAssetAddress: SWORD_ADDRESS
});
const modifyTxResp = await nodeClient.signAndSubmitTransaction({
signer: collectionCreator,
transaction: modifyTx
});
console.log(modifyTxResp);
Here is my transaction, and here is the updated sword:
Add an attribute
Now, let's say I defeat a boss and earn an enchantment on the sword! This enchantment applies a small dose of poison to enemies I attack, dealing them some extra damage.
Typescript example
const ENCHANTMENT = "enchantment";
const SMALL_POISON = 'small poison';
const newAbilityTx = await nodeClient.addDigitalAssetTypedPropertyTransaction({
creator: collectionCreator,
propertyKey: ENCHANTMENT,
propertyType: "STRING",
propertyValue: SMALL_POISON,
digitalAssetAddress: SWORD_ADDRESS
});
const newAbilityTxResp = await nodeClient.signAndSubmitTransaction({
signer: collectionCreator,
transaction: newAbilityTx
});
console.log(newAbilityTxResp);
Here is my transaction, and here is my sword now (notice I lost some more durability in my boss fight):
You can remove abilities as well.
Where the data lives
Off-chain data storage
While NFTs exist on chain, sometimes part of their data does not. The typical off-chain data would be any associated large media files (image, video, audio) - e.g. the full size and thumbnail image of a hero character.
Examples of off-chain storage include:
- A decentralized storage network such as
- Centralized storage like an AWS S3 bucket + CloudFront CDN.
Caching
The ultimate truth about an NFT’s ownership and attributes lives in the latest state of the Aptos blockchain. However, frequently read data can often be cached (especially in cases where your app controls minting and updating NFTs, and so knows when NFT data changes).
Ownership, transfer, and sale of NFTs
Ownership
Games use Shinami Invisible (NFT) Wallets to hold its users NFTs. In both cases, the user doesn't need to remember a seed phrase to manage their wallet - they just need to know how to log into your app. Your app determines when and how to tell the user their in-game object is an NFT on the Aptos blockchain. For a richer comparison overview of how our Invisible Wallets work, see our high-level guide.
Sale and transfer
If using Shinami Invisible Wallets, your app can choose when to present the user with an option to sell or trade the NFT to another player. Your app can also provide a means for users to take control of their keys by, for example, creating a cross-app, Aptos Connect wallet, and transferring the NFT to it.
If you create a royalty, then you can make money if a player sells an item on a NFT marketplace.
Resources
Official standards and resources
- The Aptos Digital Assest (DA) Standard is the current standard for creating NFTs on Aptos. It's made possible by the Aptos Object Model.
- This standard replaced the legacy Aptos Token Standard.
Developer Tutorials and sample code
- Aptos Labs guide to minting your first NFT.
- Aptos Learn site tutorial to create an NFT marketplace.
- Aptos Labs token object examples sample code has a rich set of examples for building nested NFTs with complex behavior (as well as soul-bound NFTs in the
ambassador
folder).
Aptos NFT marketplaces
- You can find a list of marketplaces here.
Updated 2 days ago