Skip to main content
Version: v5.0.0-nightly.20260305

AIP-721: Non-Fungible Token

Source

AIP-721 defines a non-fungible token (NFT). Each token is identified by a unique token_id field. Tokens can be held privately in the note hash tree or publicly in a map from token_id to owner address.

Storage layout

#[storage]
struct Storage<Context> {
symbol: PublicImmutable<FieldCompressedString, Context>,
name: PublicImmutable<FieldCompressedString, Context>,
private_nfts: Owned<PrivateSet<NFTNote, Context>, Context>,
nft_exists: Map<Field, PublicMutable<bool, Context>, Context>,
public_owners: Map<Field, PublicMutable<AztecAddress, Context>, Context>,
minter: PublicImmutable<AztecAddress, Context>,
upgrade_authority: PublicImmutable<AztecAddress, Context>,
}

nft_exists tracks whether a given token_id has been minted, while public_owners records the current public owner. When an NFT is moved to a private note, the public_owners entry is cleared and the NFT is stored as an NFTNote in the holder's private set.

NFTNote and partial-note support

Each private NFT is represented as an NFTNote containing only the token_id:

#[custom_note]
pub struct NFTNote {
pub token_id: Field,
}

impl NFTNote {
pub fn partial(
owner: AztecAddress,
storage_slot: Field,
context: &mut PrivateContext,
recipient: AztecAddress,
completer: AztecAddress,
) -> PartialNFTNote {
let randomness = unsafe { random() };
let commitment = compute_partial_commitment(owner, storage_slot, randomness);
// ... creates encrypted log and validity commitment
let partial_note = PartialNFTNote { commitment };
let validity_commitment = partial_note.compute_validity_commitment(completer);
context.push_nullifier(validity_commitment);
partial_note
}
}

The partial constructor creates a PartialNFTNote whose commitment field commits to the future owner and storage slot. Without some form of access control, any party could call the completion function and claim the NFT for themselves. The validity commitment prevents this — it is pushed as a nullifier, and only the designated completer can produce the matching preimage needed to finalize the note. This mirrors the partial-note pattern in AIP-20 but applies it to NFT transfers.

Partial-note transfer commitment

The external entry point for initiating a partial NFT transfer is:

#[external("private")]
fn initialize_transfer_commitment(to: AztecAddress, completer: AztecAddress) -> Field {
let commitment = self.internal._initialize_transfer_commitment(to, completer);
commitment.commitment()
}

This function returns commitment.commitment() (the raw commitment field), whereas the AIP-20 equivalent returns commitment.to_field(). The difference reflects the distinct internal types — PartialNFTNote vs PartialUintNote — but both return an opaque Field to the caller.