An applied guide to NFTs Pallet
This guide assumes you have basic knowledge of Polkadot and understand fundamental terminology such as pallets and extrinsics.
All coding examples use Polkadot-API (PAPI). However, this guide doesn't cover the basics like initializing PAPI or creating a signer. You can start by reading the official PAPI documentation, or if you prefer learning by doing, explore the provided code examples or start with one of the available templates.
The NFTs pallet is the primary method for creating all types of NFTs on Polkadot. This guide explores its capabilities in depth. Learn about other available options in the overview section.
Collections
On Polkadot, NFTs begin with a collection, which serves as a container for future tokens. Every collection has a unique numeric ID that increments each time a new collection is created.
Creating a collection
Creating a collection requires making a deposit.
When creating a collection, you need to specify:
admin
: the collection's administrator who has the rights to manage the collection. Read about collection roles.- collection
config
: a set of rules and configurations for the collection.
Creating a collection with PAPI looks like this:
const createCollectionTx = await api.tx.Nfts.create({
admin: MultiAddress.Id(alice.address),
config: {
max_supply: 1000,
mint_settings: {
default_item_settings: 0n,
mint_type: Enum("Issuer"),
price: undefined,
start_block: undefined,
end_block: undefined,
},
settings: 0n,
},
}).signAndSubmit(collectionOwner);
When the collection is created, you can get its ID from the emitted events:
...
const [createdEvent] = api.event.Nfts.Created.filter(createCollectionTx.events);
const collectionId = createdEvent.collection;
You can also query the list of collections owned by account:
const ownedCollections = await api.query.Nfts.CollectionAccount.getEntries(
alice.address
);
const collectionIds = ownedCollections.map((c) => c.keyArgs[1]);
Let's also examine what the max_supply
, mint_settings
, and settings
fields mean.
Collection configuration
During collection creation, you need to specify a set of rules and configurations for the collection. This configuration is set during creation and can be modified later by the collection team if not locked.
Maximum collection supply
max_supply
is the maximum number of tokens that can be minted. This setting can be omitted if the collection doesn't have a supply limit.
const createCollectionTx = await api.tx.Nfts.create({
admin: MultiAddress.Id(alice.address),
config: {
max_supply: 1000,
...
},
}).signAndSubmit(collectionOwner);
The collection owner can modify the maximum collection supply if it's not locked by collection settings.
const setMaxSupplyTx = await api.tx.Nfts.set_collection_max_supply({
max_supply: 500,
collection: collectionId,
}).signAndSubmit(collectionOwner);
Collection settings and locks
Collection settings
can be specified in bitflag format for a collection. They define the locked status of:
- NFT transfers: Controls whether items can be transferred between accounts. When set, items become non-transferable (soulbound). You can still mint tokens, however.
- Collection metadata: When set, metadata becomes permanently locked and cannot be set or changed. This setting applies only to collection metadata; token metadata mutability is defined separately.
- Collection attributes: When set, collection attributes become permanently locked. This setting applies only to collection attributes; token attribute mutability is defined separately.
- Collection max supply: When set, the max supply becomes permanently fixed.
const createCollectionTx = await api.tx.Nfts.create({
admin: MultiAddress.Id(alice.address),
config: {
...
settings: 0n,
},
}).signAndSubmit(collectionOwner);
Examples:
0
(0000): All settings unlocked15
(1111): All settings locked9
(1001): Locked max supply and tokens are non-transferable
Collection settings can be locked but never unlocked. You may want to leave them mutable (0
) and lock them later, for example, after all NFTs are minted.
Click to see the full list of collection settings bitflags
Value | Binary | Transferable NFTs | Mutable Collection Metadata | Mutable Collection Attributes | Mutable Max Supply |
---|---|---|---|---|---|
0 | 0000 | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
1 | 0001 | 🔒 No | ✅ Yes | ✅ Yes | ✅ Yes |
2 | 0010 | ✅ Yes | 🔒 No | ✅ Yes | ✅ Yes |
3 | 0011 | 🔒 No | 🔒 No | ✅ Yes | ✅ Yes |
4 | 0100 | ✅ Yes | ✅ Yes | 🔒 No | ✅ Yes |
5 | 0101 | 🔒 No | ✅ Yes | 🔒 No | ✅ Yes |
6 | 0110 | ✅ Yes | 🔒 No | 🔒 No | ✅ Yes |
7 | 0111 | 🔒 No | 🔒 No | 🔒 No | ✅ Yes |
8 | 1000 | ✅ Yes | ✅ Yes | ✅ Yes | 🔒 No |
9 | 1001 | 🔒 No | ✅ Yes | ✅ Yes | 🔒 No |
10 | 1010 | ✅ Yes | 🔒 No | ✅ Yes | 🔒 No |
11 | 1011 | 🔒 No | 🔒 No | ✅ Yes | 🔒 No |
12 | 1100 | ✅ Yes | ✅ Yes | 🔒 No | 🔒 No |
13 | 1101 | 🔒 No | ✅ Yes | 🔒 No | 🔒 No |
14 | 1110 | ✅ Yes | 🔒 No | 🔒 No | 🔒 No |
15 | 1111 | 🔒 No | 🔒 No | 🔒 No | 🔒 No |
The collection owner can change these settings by locking them. Remember – this operation cannot be undone – a locked setting remains locked forever.
await api.tx.Nfts.lock_collection({
collection: collectionId,
lock_settings: 15n,
}).signAndSubmit(collectionOwner);
NFT minting settings
The rules related to token minting include:
mint_type
: who can mint tokens.
Issuer
: Only the collection issuer can mintPublic
: Everyone can mintHolderOf
: Only holders of NFTs in the specified collection can mint
price
: The price of token minting that will be paid to the collection owner. This can be useful forPublic
minting.start_block
andend_block
: allow you to specify a timeframe when minting is alloweddefault_item_settings
: the default settings that define whether future items can be transferred and whether item attributes and metadata are mutable.
Once metadata, attributes, or transfers are locked, it's not possible to unlock them. By default, you may want to set default_item_settings
to 0
and modify them later.
Click to see the full list of item settings bitflags
Value | Binary | Mutable attributes | Mutable metadata | Transferable |
---|---|---|---|---|
0 | 000 | ✅ Yes | ✅ Yes | ✅ Yes |
1 | 001 | ✅ Yes | ✅ Yes | 🔒 No |
2 | 010 | ✅ Yes | 🔒 No | ✅ Yes |
3 | 011 | ✅ Yes | 🔒 No | 🔒 No |
4 | 100 | 🔒 No | ✅ Yes | ✅ Yes |
5 | 101 | 🔒 No | ✅ Yes | 🔒 No |
6 | 110 | 🔒 No | 🔒 No | ✅ Yes |
7 | 111 | 🔒 No | 🔒 No | 🔒 No |
Collection metadata
Setting collection metadata requires making a deposit.
Collection-level metadata can be added to or removed from a collection by the collection admin if it's not locked. While there are no enforced formatting rules, you'll most likely want to use it similarly to how contractURI
is used in Ethereum NFT standards. You can set a link to IPFS or any other off-chain storage, or store this metadata on-chain.
{
"name": "My collection name",
"description": "This is my cool collection",
"image": "https://some-external-storage.com/image.png"
}
Collection metadata can be set after the collection is created:
await api.tx.Nfts.set_collection_metadata({
collection: collectionId,
data: Binary.fromText("https://some-external-storage.com/metadata.json"),
}).signAndSubmit(collectionAdmin);
The collection admin can clear metadata:
await api.tx.Nfts.clear_collection_metadata({
collection: collectionId,
}).signAndSubmit(collectionAdmin);
There's a separate method to query collection metadata:
const collectionMetadata = await api.query.Nfts.CollectionMetadataOf.getValue(
collectionId
);
Collection attributes
Setting collection attributes requires making a deposit.
Collection attributes are on-chain key-value pairs of arbitrary data. The collection admin can set or remove attributes of a collection if the collection is not locked.
await api.tx.Nfts.set_attribute({
collection: collectionId,
key: Binary.fromText("Key"),
value: Binary.fromText("Attribute value"),
namespace: Enum("CollectionOwner"),
maybe_item: undefined,
}).signAndSubmit(owner);
You can query a collection attribute:
const collectionAttribute = await api.query.Nfts.Attribute.getValue(
collectionId,
undefined,
Enum("CollectionOwner"),
Binary.fromText("Key")
);
The collection admin can clear a collection attribute if attributes are not locked:
await api.tx.Nfts.clear_attribute({
collection: collectionId,
key: Binary.fromText("test"),
namespace: Enum("CollectionOwner"),
maybe_item: undefined,
}).signAndSubmit(collectionAdmin);
Destroying a collection
The collection owner can destroy a collection if there are no items. If the collection has attributes or item metadata set, their amounts should be provided in the witness:
await api.tx.Nfts.destroy({
collection: collectionId,
witness: {
attributes: 2,
item_configs: 1,
item_metadatas: 1,
},
}).signAndSubmit(owner);
You can query witness data before destroying a collection:
const witness = await api.query.Nfts.Collection.getValue(collectionId);
Collection roles
There are four roles that exist for a collection.
Owner
The collection creator becomes the owner of the collection. The owner can:
- Set team: change the collection's
Issuer
,Admin
, andFreezer
. Be careful—after setting a role to None, it cannot be set again. - Destroy collection: only if there are no items in the collection.
- Set collection max supply: set the maximum number of items for a collection.
- Redeposit: re-evaluate the deposit on some items.
- Change collection settings: make NFTs non-transferable, lock metadata and attributes.
The collection owner can set the other roles:
const setTeamTx = await api.tx.Nfts.set_team({
collection: collectionId,
admin: MultiAddress.Id(charlie.address),
issuer: MultiAddress.Id(dave.address),
freezer: MultiAddress.Id(eve.address),
}).signAndSubmit(collectionOwner);
The roles are stored as a bitflag and can be queried for an account:
const roles = await api.query.Nfts.CollectionRoleOf.getValue(
collectionId,
account.address
);
// roles is a bitflag
The following bitflags can be set:
Value | Binary | Admin | Freezer | Issuer |
---|---|---|---|---|
1 | 001 | 🔒 No | 🔒 No | ✅ Yes |
2 | 010 | 🔒 No | ✅ Yes | 🔒 No |
3 | 011 | 🔒 No | ✅ Yes | ✅ Yes |
4 | 100 | ✅ Yes | 🔒 No | 🔒 No |
5 | 101 | ✅ Yes | 🔒 No | ✅ Yes |
6 | 110 | ✅ Yes | ✅ Yes | 🔒 No |
7 | 111 | ✅ Yes | ✅ Yes | ✅ Yes |
Ownership can be transferred in two steps:
- The future recipient should accept ownership:
const receiveTx = await api.tx.Nfts.set_accept_ownership({
maybe_collection: collectionId,
}).signAndSubmit(receiver);
expect(receiveTx.ok).toBe(true);
- After that, the collection owner can transfer:
const transferTx = await api.tx.Nfts.transfer_ownership({
collection: collectionId,
new_owner: MultiAddress.Id(charlie.address),
}).signAndSubmit(owner);
The deposit associated with the collection will be transferred to the new owner.
Admin
A collection Admin
is the only role set during collection creation. Until other roles are set after collection creation, an Admin
receives Issuer
and Freezer
roles as well.
The Admin can:
- Set collection attributes and metadata
- Clear collection metadata
- Set collection item attributes and metadata
- Lock item properties (metadata and attributes)
Freezer
The Freezer can:
- Lock item transfers
- Unlock item transfers
Issuer
The Issuer can:
- Mint, force mint, and mint pre-signed
- Update mint settings
Items (NFTs)
Now that you understand collections, let's explore NFTs themselves - the individual items within collections. This section covers the complete lifecycle of NFTs: minting tokens with various authorization methods, managing metadata and attributes for rich token properties, transferring ownership through direct and approved mechanisms, implementing access controls through locking mechanisms, and facilitating peer-to-peer trading without external marketplaces.
NFT minting
NFT minting requires making a deposit.
Collection items (NFTs) can be created depending on minting settings:
Issuer
: Only the collection issuer can mintPublic
: Everyone can mintHolderOf
: Only holders of NFTs in the specified collection can mint
The account eligible for minting needs to provide a unique item ID. Additionally, witness data should be provided in the following cases:
- Mint types set to
HolderOf
, then the item ID of the specified collection should be provided - Mint
price
is set – then the price of minting should be provided.
const mintTx = await api.tx.Nfts.mint({
collection: collectionId,
item: 1,
mint_to: MultiAddress.Id(bob.address),
witness_data: {
mint_price: 5n * 10n ** 10n, // If the mint price is 5 DOT
owned_item: 2,
},
}).signAndSubmit(alice);
Check that the transaction has been successful or search for a special event to make sure the NFT has been minted successfully:
const [issuedEvent] = api.event.Nfts.Issued.filter(createItemTx.events);
Presigned minting
Presigned minting allows collection issuers to create off-chain mint authorizations that others can execute on-chain. This is particularly useful for:
- Whitelisted NFT drops where specific users get guaranteed minting rights
- Marketplace integrations where third parties mint on behalf of creators
- Time-limited campaigns with automatic expiration
- Decentralized claiming where users mint when convenient rather than receiving airdrops
To create a presigned mint authorization, the issuer prepares mint data and signs it off-chain:
import { MultiSignature, dot } from "@polkadot-api/descriptors";
import { getTypedCodecs } from "polkadot-api";
// Get codecs for encoding mint data
const codecs = await getTypedCodecs(dot);
const mintDataCodec = codecs.tx.Nfts.mint_pre_signed.inner.mint_data;
// Prepare mint data
const mintData = {
collection: collectionId,
item: 1,
attributes: [
[Binary.fromText("tier"), Binary.fromText("gold")],
[Binary.fromText("whitelist"), Binary.fromText("true")],
] as FixedSizeArray<2, Binary>[],
metadata: Binary.fromText("Presigned NFT #1"),
only_account: bob.address, // Optional: restrict to specific account
deadline: 10_000_000, // Block number when authorization expires
mint_price: undefined, // Optional: set price requirements
};
// Encode and sign the data
const encodedData = mintDataCodec.enc(mintData);
const signature = await issuer.signBytes(encodedData);
The authorized user can then execute the mint:
const mintTx = await api.tx.Nfts.mint_pre_signed({
mint_data: mintData,
signature: MultiSignature.Sr25519(FixedSizeBinary.fromBytes(signature)),
signer: issuer.address,
}).signAndSubmit(bob);
Key parameters for presigned minting:
only_account
: Restricts who can use the authorization. If undefined, anyone can use it.deadline
: Block number after which the authorization expiresmint_price
: Optional payment required from the minterattributes
: Attributes to set during minting
NFT metadata
Setting item metadata requires making a deposit.
The collection Admin can set or remove an item's metadata if it's not locked. Most likely you'll want to use metadata the same way as tokenURI
in the ERC-721 metadata extension. You can set a link to IPFS or any other off-chain storage.
{
"name": "My NFT",
"description": "The description here",
"image": "https://some-external-storage.com/image.png",
"attributes": [
{
"trait_type": "Color",
"value": "Red"
},
{
"trait_type": "Size",
"value": "M"
}
]
}
Once your metadata is uploaded, the collection admin can attach the link to the item:
const setMetadataTx = await api.tx.Nfts.set_metadata({
collection: collectionId,
data: Binary.fromText("https://external-storage.com/metadata.json"),
item: 1,
}).signAndSubmit(collectionAdmin);
You can then query the item's metadata and the amount of deposit:
const metadata = await api.query.Nfts.ItemMetadataOf.getValue(collectionId, 1);
const metadata = metadata?.data.asText(); // "https://example.com"
const deposit = metadata?.deposit; // {account: ..., amount: ...}
There's a dedicated method to clear item metadata. The deposit associated with metadata will be released:
const clearMetadataTx = await api.tx.Nfts.clear_metadata({
collection: collectionId,
data: Binary.fromText("https://external-storage.com/metadata.json"),
item: 1,
}).signAndSubmit(collectionAdmin);
NFT attributes
Setting item attributes requires making a deposit.
NFT attributes are on-chain key-value pairs of arbitrary data. This is particularly useful when some characteristics should be mutable, for example in gaming applications.
The collection admin or item owner can set or remove attributes of an item if the item is not locked. To specify who can modify attributes, you need to set a namespace:
const collectionOwnerAttribute = await api.tx.Nfts.set_attribute({
collection: collectionId,
maybe_item: 1,
namespace: Enum("CollectionOwner"),
key: Binary.fromText("Experience"),
value: Binary.fromText("300"),
}).signAndSubmit(collectionAdmin);
const itemOwnerAttributeTx = await api.tx.Nfts.set_attribute({
collection: collectionId,
maybe_item: 1,
namespace: Enum("ItemOwner"),
key: Binary.fromText("Owner"),
value: Binary.fromText("Bob"),
}).signAndSubmit(itemOwner);
You can then query item attributes and deposits:
// Get all attributes
const attributes = await api.query.Nfts.Attribute.getEntries(
collectionId,
itemId
);
// Or just a single attribute
const attribute = await api.query.Nfts.Attribute.getValue(
collectionId,
1,
Enum("CollectionOwner"),
Binary.fromText("Experience")
);
To clear an unlocked attribute, any signer that conforms to the namespace ruleset can execute the dedicated method. The deposit associated with the attribute will be released:
const clearAttributeTx = await api.tx.Nfts.clear_attribute({
collection: collectionId,
maybe_item: 1,
key: Binary.fromText("Experience"),
namespace: Enum("CollectionOwner"),
}).signAndSubmit(alice);
Setting presigned attributes
Presigned attributes enable authorized parties to create off-chain attribute authorizations that item owners can apply on-chain. This is particularly useful for scenarios like reveals and delayed attribute applications.
To create a presigned attribute authorization, an authorized signer (collection admin for CollectionOwner namespace) prepares attribute data and signs it off-chain:
import { MultiSignature, dot } from "@polkadot-api/descriptors";
import { getTypedCodecs, FixedSizeArray, Binary } from "polkadot-api";
// Get codecs for encoding attribute data
const codecs = await getTypedCodecs(dot);
const attributeDataCodec = codecs.tx.Nfts.set_attributes_pre_signed.inner.data;
// Prepare attribute data
const attributeData = {
collection: collectionId,
item: 1,
deadline: 10_000_000, // Block number when authorization expires
namespace: Enum("CollectionOwner"),
attributes: [
[Binary.fromText("Experience"), Binary.fromText("300")],
[Binary.fromText("Power"), Binary.fromText("200")],
] as FixedSizeArray<2, Binary>[],
};
// Encode and sign the data
const encodedData = attributeDataCodec.enc(attributeData);
const signature = await collectionAdmin.signBytes(encodedData);
The item owner can then apply the presigned attributes:
const setAttributesPresignedTx = await api.tx.Nfts.set_attributes_pre_signed({
data: attributeData,
signature: MultiSignature.Sr25519(FixedSizeBinary.fromBytes(signature)),
signer: collectionAdmin.address,
}).signAndSubmit(itemOwner);
Key parameters for presigned attributes:
deadline
: Block number after which the authorization expiresnamespace
: OnlyCollectionOwner
andAccount
namespaces are supportedattributes
: Up to 10 key-value pairs can be set in a single transactionsigner
: Must be authorized for the specified namespace (collection admin forCollectionOwner
)
The transaction will fail if the deadline has passed, the signer lacks proper authorization, or the maximum attribute limit is exceeded.
Approving attribute modifications
Item owners can approve other accounts to set attributes in the Account
namespace for their NFTs. This is particularly useful for scenarios where you want to allow trusted third parties to modify certain attributes on your behalf, such as in gaming applications where game servers need to update player stats.
Multiple accounts can be approved for the same item:
// Approve first delegate
await api.tx.Nfts.approve_item_attributes({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(charlie.address),
}).signAndSubmit(itemOwner);
// Approve second delegate
await api.tx.Nfts.approve_item_attributes({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(dave.address),
}).signAndSubmit(itemOwner);
You can check which accounts are approved to set attributes for an item:
const approvals = await api.query.Nfts.ItemAttributesApprovalsOf.getValue(
collectionId,
itemId
);
// Returns an array of approved account addresses
Once approved, the delegate can set attributes using the Account
namespace:
const setAttributeTx = await api.tx.Nfts.set_attribute({
collection: collectionId,
maybe_item: 1,
namespace: Enum("Account", delegate.address),
key: Binary.fromText("GameScore"),
value: Binary.fromText("1500"),
}).signAndSubmit(delegate);
Only the Account
namespace is supported. Attributes in different namespaces cannot be modified by approved accounts.
Item owners can revoke approval at any time. The witness parameter should match or exceed the actual number of attributes set by the delegate to avoid transaction failure.
When canceling approval, all attributes set by the delegate in the Account
namespace will be permanently removed.
const cancelApprovalTx = await api.tx.Nfts.cancel_item_attributes_approval({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(delegate.address),
witness: 2, // Number of attributes set by this delegate
}).signAndSubmit(itemOwner);
NFT transfer
NFT owners can transfer their tokens to other accounts either directly or through an approval system that allows designated delegates to execute transfers on their behalf.
Direct transfer
The NFT owner can directly transfer their token to any account:
const transferTx = await api.tx.Nfts.transfer({
collection: collectionId,
item: 1,
dest: MultiAddress.Id(charlie.address),
}).signAndSubmit(currentOwner);
After a successful transfer, you can verify the new ownership:
const itemData = await api.query.Nfts.Item.getValue(collectionId, 1);
const newOwner = itemData?.owner; // charlie.address
Approved transfers
NFT owners can approve multiple accounts to transfer their tokens on their behalf. This is useful for marketplaces, escrow services, or allowing trusted parties to manage transfers.
Multiple accounts can be approved for the same NFT:
// Approve first delegate
await api.tx.Nfts.approve_transfer({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(charlie.address),
maybe_deadline: undefined,
}).signAndSubmit(nftOwner);
// Approve second delegate
await api.tx.Nfts.approve_transfer({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(dave.address),
maybe_deadline: undefined,
}).signAndSubmit(nftOwner);
You can check the current approvals for an NFT:
const itemData = await api.query.Nfts.Item.getValue(collectionId, 1);
const approvals = itemData?.approvals; // Array of approved delegates
Once approved, any delegate can execute the transfer using the same transfer
method:
const delegatedTransferTx = await api.tx.Nfts.transfer({
collection: collectionId,
item: 1,
dest: MultiAddress.Id(newOwner.address),
}).signAndSubmit(approvedDelegate);
When a transfer is executed (either direct or approved), all existing transfer approvals for that NFT are automatically cleared.
Managing transfer approvals
NFT owners can cancel specific approvals:
const cancelApprovalTx = await api.tx.Nfts.cancel_approval({
collection: collectionId,
item: 1,
delegate: MultiAddress.Id(delegate.address),
}).signAndSubmit(nftOwner);
Or clear all transfer approvals at once:
const clearAllApprovalsTx = await api.tx.Nfts.clear_all_transfer_approvals({
collection: collectionId,
item: 1,
}).signAndSubmit(nftOwner);
Transfer restrictions
NFT transfers can be restricted at the collection level through collection settings or token level. When transfers are locked, NFTs become non-transferable (soulbound) and cannot be moved between accounts, though they can still be minted and burned.
NFT burn
The item owner can burn
an NFT:
await api.tx.Nfts.burn({
collection: collectionId,
item: itemId,
}).signAndSubmit(itemOwner);
Locking NFTs
NFTs can be locked to restrict certain behaviors and ensure immutability. The following properties can be locked:
- Transferability: Prevents NFTs from being transferred between accounts (making them soulbound)
- Metadata mutability: Permanently prevents changes to NFT metadata
- Attributes mutability: Permanently prevents changes to NFT attributes
When minting NFTs, the default_item_settings
from the collection's mint settings will be applied to each newly created item.
You can check an item's current lock status:
const itemSettings = await api.query.Nfts.ItemConfigOf.getValue(
collectionId,
1
);
// itemSettings is a bitflag representing the lock status
Click to see the full list of item settings bitflags
Value | Binary | Mutable attributes | Mutable metadata | Transferable |
---|---|---|---|---|
0 | 000 | ✅ Yes | ✅ Yes | ✅ Yes |
1 | 001 | ✅ Yes | ✅ Yes | 🔒 No |
2 | 010 | ✅ Yes | 🔒 No | ✅ Yes |
3 | 011 | ✅ Yes | 🔒 No | 🔒 No |
4 | 100 | 🔒 No | ✅ Yes | ✅ Yes |
5 | 101 | 🔒 No | ✅ Yes | 🔒 No |
6 | 110 | 🔒 No | 🔒 No | ✅ Yes |
7 | 111 | 🔒 No | 🔒 No | 🔒 No |
Locking transfers
Transfer restrictions can be applied at both collection and item levels:
Collection-level transfer locks can be set by the collection owner. This affects all items in the collection and is permanent—it cannot be undone.
Item-level transfer locks can be managed by the collection freezer and can be both locked and unlocked as needed.
// Lock transfers for a specific NFT
await api.tx.Nfts.lock_item_transfer({
collection: collectionId,
item: itemId,
}).signAndSubmit(freezer);
// Unlock transfers for a specific NFT
await api.tx.Nfts.unlock_item_transfer({
collection: collectionId,
item: itemId,
}).signAndSubmit(freezer);
Locking metadata and attributes
Item metadata and attributes can be permanently locked by the collection admin. Once locked, these properties become immutable and cannot be changed.
This operation is irreversible. Once metadata or attributes are locked, they cannot be unlocked or modified.
await api.tx.Nfts.lock_item_properties({
collection: collectionId,
item: itemId,
lock_metadata: true,
lock_attributes: true,
}).signAndSubmit(admin);
Trading
The NFTs pallet includes built-in trading capabilities that enable direct peer-to-peer transactions without requiring external marketplaces or intermediaries. The trading system supports both simple buy/sell operations and sophisticated item swaps with optional additional payments.
Setting an NFT Price
NFT owners can list their tokens for sale by setting a price. This creates an immediate purchase opportunity for any interested party:
const setPriceTx = await api.tx.Nfts.set_price({
collection: collectionId,
item: itemId,
price: 1n * 10n ** 10n, // 1 DOT
whitelisted_buyer: undefined, // Optional: restrict to specific buyer
}).signAndSubmit(nftOwner);
The whitelisted_buyer
parameter allows sellers to restrict purchases to a specific account, enabling private sales or exclusive offers.
You can query the current listing price:
const itemPrice = await api.query.Nfts.ItemPriceOf.getValue(collectionId, 1);
const currentPrice = itemPrice?.[0]; // Returns price or undefined if not for sale
Buying an NFT
Once an NFT is listed for sale, any account can purchase it by matching or exceeding the asking price:
const buyItemTx = await api.tx.Nfts.buy_item({
collection: collectionId,
item: itemId,
bid_price: ITEM_PRICE, // Must be >= the listed price
}).signAndSubmit(buyer);
The transaction will:
- Transfer ownership of the NFT to the buyer
- Transfer the payment to the seller
- Automatically remove the item from sale
- Clear any existing transfer approvals
Withdrawing from Sale
NFT owners can remove their items from the marketplace at any time by setting the price to undefined
:
const withdrawTx = await api.tx.Nfts.set_price({
collection: collectionId,
item: itemId,
price: undefined, // Removes from sale
whitelisted_buyer: undefined,
}).signAndSubmit(nftOwner);
Once withdrawn, purchase attempts will fail with a NotForSale
error.
NFT Swapping
The swapping system enables complex peer-to-peer trades where users can exchange NFTs from different collections, optionally with additional payments flowing in either direction.
Creating a Swap Offer
To initiate a swap, specify what you're offering and what you want in return:
const createSwapTx = await api.tx.Nfts.create_swap({
offered_collection: collectionIdA,
offered_item: itemIdA,
desired_collection: collectionIdB,
maybe_desired_item: undefined, // Any item from collection, or specify item ID
maybe_price: {
amount: 5n * 10n ** 10n, // 0.5 DOT additional payment
direction: Enum("Receive"), // "Send" or "Receive"
},
duration: 1000, // Swap expires after 1000 blocks
}).signAndSubmit(swapCreator);
Key Parameters:
maybe_desired_item
: Set toundefined
to accept any item from the collection, or specify an exact item IDmaybe_price
: Optional additional payment. Use"Send"
if you'll pay extra,"Receive"
if you want to receive paymentduration
: Number of blocks until the swap offer expires
You can query pending swaps:
const swap = await api.query.Nfts.PendingSwapOf.getValue(collectionId, itemId);
// Returns swap details or undefined if no pending swap exists
Claiming a Swap
Anyone holding a suitable NFT can claim an active swap:
const claimSwapTx = await api.tx.Nfts.claim_swap({
send_collection: collectionIdB,
send_item: itemIdB, // Claimer's item in collection B
receive_collection: collectionIdA,
receive_item: itemIdA,
witness_price: {
amount: 5n * 10n ** 10n,
direction: Enum("Receive"),
},
}).signAndSubmit(swapClaimer);
The witness_price
must match the original swap terms exactly. After a successful claim:
- Both NFTs change ownership
- Any additional payment is transferred
- The swap is automatically removed from pending swaps
Canceling Swap Offers
Cancel your own swap offers:
const cancelSwapTx = await api.tx.Nfts.cancel_swap({
offered_collection: collectionId,
offered_item: itemId,
}).signAndSubmit(swapCreator);
Queries
Getting account NFTs
You can get the list of all NFTs owned by an account. The following query returns collection and item IDs.
const items = await api.query.Nfts.Account.getEntries(bob.address);
const itemList = items.map((item) => ({
collectionId: item.keyArgs[1],
itemId: item.keyArgs[2],
}));
Getting a specific NFT
You can also get information about a specific item. The following query returns data about the NFT owner, approvals, and deposits:
// Get info about item 1
const singleItem = await api.query.Nfts.Item.getValue(collectionId, 1);
// Or you can query multiple items:
const manyItems = await api.query.Nfts.Item.getValues([
[collectionId, 1],
[collectionId, 2],
]);
Getting NFT attributes
// Query all attributes
const attributes = await api.query.Nfts.Attribute.getEntries(collectionId, 1);
// Query a specific attribute with "Experience" key
const attribute = await api.query.Nfts.Attribute.getValue(
collectionId,
1,
Enum("CollectionOwner"),
Binary.fromText("Experience")
);
Getting NFT metadata
const metadata = await api.query.Nfts.ItemMetadataOf.getValue(collectionId, 1);
Deposits
To prevent blockchain bloat, deposits must be reserved for certain actions. These deposits can be released when the associated storage is cleared or ownership is transferred.
Deposit amounts for collection and item creation are fixed (but may change in future updates). You can query deposit amounts as constants using PAPI:
api.constants.Nfts.CollectionDeposit()
api.constants.Nfts.ItemDeposit()
api.constants.Nfts.MetadataDepositBase()
api.constants.Nfts.AttributeDepositBase()
api.constants.Nfts.DepositPerByte()
Current deposit amounts:
- Collection creation: base amount of
0.2013
DOT - Item creation: base amount of
0.005041
DOT - Setting metadata for a collection/item: base amount of
0.020129
DOT, plus an additional0.00001
DOT per byte - Setting attributes for a collection/item: base amount of
0.02
DOT, plus an additional0.00001
DOT per byte
Batching
The NFTs pallet supports transaction batching through Polkadot's Utility
pallet, allowing you to execute multiple NFT operations atomically in a single transaction. This is particularly useful for complex workflows like collection setup with metadata and attributes.
Atomic Batch Execution
Use batch_all
to ensure all operations succeed together or fail together. If any transaction in the batch fails, the entire batch is reverted:
// Get the next collection ID before batching
const nextCollectionId = await api.query.Nfts.NextCollectionId.getValue();
// Prepare individual transactions as decoded calls
const createCollectionTx = api.tx.Nfts.create({
admin: MultiAddress.Id(alice.address),
config: {
max_supply: 1000,
mint_settings: {
default_item_settings: 0n,
mint_type: Enum("Issuer"),
price: undefined,
start_block: undefined,
end_block: undefined,
},
settings: 0n,
},
}).decodedCall;
const setCollectionMetadataTx = api.tx.Nfts.set_collection_metadata({
collection: nextCollectionId,
data: Binary.fromText("https://example.com/collection.json"),
}).decodedCall;
const setCollectionAttributeTx = api.tx.Nfts.set_attribute({
collection: nextCollectionId,
key: Binary.fromText("category"),
value: Binary.fromText("gaming"),
namespace: Enum("CollectionOwner"),
maybe_item: undefined,
}).decodedCall;
// Execute all operations atomically
const batchTx = await api.tx.Utility.batch_all({
calls: [
createCollectionTx,
setCollectionMetadataTx,
setCollectionAttributeTx,
],
}).signAndSubmit(alice);