Skip to main content

Non-Fungible Tokens (NFT)

In contrast with fungible tokens, non-fungible tokens (NFT) are unitary and therefore unique. This makes NFTs ideal to represent ownership of assets such as a piece of digital content, or a ticket for an event.

As with fungible tokens, NFTs are not stored in the user's wallet, instead, each NFT lives in a NFT contract. The NFT contract works as a bookkeeper, this is: it is in charge of handling the creation, storage and transfers of NFTs.

In order for a contract to be considered a NFT-contract it has to follow the NEP-171 and NEP-177 standards. The NEP-171 & NEP-177 standards explain the minimum interface required to be implemented, as well as the expected functionality.

NFT & Marketplaces

Be mindful of not confusing an NFT with an NFT-marketplace. NFT simply store information (metadata), while NFT-marketplaces are contracts where NFT can be listed and exchanged for a price.


About the NFT Standard​

NEP-171 is the foundational standard for Non-Fungible Tokens (NFTs) on the NEAR blockchain. It defines a common set of rules that all NFT contracts must follow, ensuring interoperability, predictability, and a consistent experience for users, wallets, and marketplaces.

The core philosophy of NEP-171 is simplicity and gas efficiency (the cost of viewing or querying data is free). It provides a minimal, event-driven skeleton, leaving more complex logic (like sale mechanics or detailed metadata) to other complementary standards.

Due to its minimalist design, the NEP-171 standard only mandates three core functionalities:

  • nft_transfer: Transfer an NFT to a new owner.

  • nft_token: The primary view method to query all information about a specific token (its ID, owner, and approved accounts).

  • Events: The contract must emit standardized events for all transfers and approvals. This allows indexers and applications to reliably track NFT activity without scanning every transaction.

Everything elseβ€”like minting, burning, enumeration (listing all tokens), and rich metadataβ€”is defined in separate, complementary standards:

  • Metadata Standard: Defines how to attach metadata (name, description, image, etc.) to the contract and individual tokens.
  • Approval Management Standard: Standardizes the mechanism that allows a token owner to grant permission to another account (or a contract) to transfer a specific token on their behalf. This is essential for listing NFTs on a marketplace..
  • Enumeration Standard: Provides methods to list all tokens, tokens by owner, etc., without which the NFTs would be unlistable.

Deploying a NFT Contract​

If you want to deploy your own NFT contract, you can create one using our reference implementation.

near deploy <account-id> --wasmFile contract.wasm --initFunction new

Global Contract​

You can deploy a new Non-Fungible Token using our global NFT contract - a pre-deployed standard NFT contract that you can reuse. Global contracts are deployed once and can be reused by any account without incurring high storage costs.

near contract deploy <account-id> use-global-account-id nft.globals.primitives.testnet \
with-init-call new \
json-args '{"owner_id": "<account-id>", "metadata": {"spec": "nft-1.0.0", "name": "MY_NFT", "symbol": "NFT2000", "icon": "data:image/svg+xml,%3Csvg xmlns='\''http://www.w3.org/2000/svg'\'' viewBox='\''0 0 288 288'\''%3E%3Cg id='\''l'\'' data-name='\''l'\''%3E%3Cpath d='\''M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'\''/%3E%3C/g%3E%3C/svg%3E"}}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
network-config testnet \
sign-with-keychain \
send
note

Deploying by hash creates an immutable contract that never changes. Deploying by account ID creates an updatable contract that changes when the referenced account's contract is updated. Choose based on whether you want your FT contract to be updatable or permanent.


Minting a NFT​

To create a new NFT (a.k.a. minting it) you will call the nft_mint method passing as arguments the metadata that defines the NFT.

// Validator interface, for cross-contract calls
#[ext_contract(ext_nft_contract)]
trait ExternalNftContract {
fn nft_mint(&self, token_series_id: String, receiver_id: AccountId) -> Promise;
}

// Implement the contract structure
#[near]
impl Contract {
#[payable]
pub fn nft_mint(&mut self, token_series_id: String, receiver_id: AccountId) -> Promise {
let promise = ext_nft_contract::ext(self.nft_contract.clone())
.with_static_gas(Gas(30*TGAS))
.with_attached_deposit(env::attached_deposit())
.nft_mint(token_series_id, receiver_id);

return promise.then( // Create a promise to callback query_greeting_callback
Self::ext(env::current_account_id())
.with_static_gas(Gas(30*TGAS))
.nft_mint_callback()
)
}

#[private] // Public - but only callable by env::current_account_id()
pub fn nft_mint_callback(&self, #[callback_result] call_result: Result<TokenId, PromiseError>) -> Option<TokenId> {
// Check if the promise succeeded
if call_result.is_err() {
log!("There was an error contacting NFT contract");
return None;
}

// Return the token data
let token_id: TokenId = call_result.unwrap();
return Some(token_id);
}
}
info

See the metadata standard for the full list of TokenMetadata parameters.

warning

Values of gas and deposit might vary depending on which NFT contract you are calling.


Minting Collections​

Many times people want to create multiple 100 copies of an NFT (this is called a collection). In such cases, what you actually need to do is to mint 100 different NFTs with the same metadata (but different token-id).

tip

Notice that minting in Mintbase allows you to pass a num_to_mint parameter.


Royalties​

You might have noticed that one of the parameters is a structure called royalties. Royalties enable you to create a list of users that should get paid when the token is sell in a marketplace. For example, if anna has 5% of royalties, each time the NFT is sell, anna should get a 5% of the selling price.


Querying NFT data​

You can query the NFT's information and metadata by calling the nft_token.

import { Wallet } from './near-wallet';

const CONTRACT_ADDRESS = 'nft.primitives.near';
const wallet = new Wallet({ createAccessKeyFor: CONTRACT_ADDRESS });

const response = await wallet.viewMethod({
method: 'nft_token',
args: {
token_id: '1',
},
});

The Wallet object comes from our quickstart template

Example response
{
"token_id": "1",
"owner_id": "bob.near",
"metadata": {
"title": "string", // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055"
"description": "string", // free-form description
"media": "string", // URL to associated media, preferably to decentralized, content-addressed storage
"media_hash": "string", // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included.
"copies": 1, // number of copies of this set of metadata in existence when token was minted.
"issued_at": 1642053411068358156, // When token was issued or minted, Unix epoch in milliseconds
"expires_at": 1642053411168358156, // When token expires, Unix epoch in milliseconds
"starts_at": 1642053411068358156, // When token starts being valid, Unix epoch in milliseconds
"updated_at": 1642053411068358156, // When token was last updated, Unix epoch in milliseconds
"extra": "string", // anything extra the NFT wants to store on-chain. Can be stringified JSON.
"reference": "string", // URL to an off-chain JSON file with more info.
"reference_hash": "string" // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}
}

Transferring a NFT​

Transferring an NFT can happen in two scenarios: (1) you ask to transfer an NFT, and (2) an authorized account asks to transfer the NFT.

In both cases, it is necessary to invoke the nft_transfer method, indicating the token id, the receiver, and an (optionally) an approval_id.

import { Wallet } from './near-wallet';

const CONTRACT_ADDRESS = 'nft.primitives.near';
const wallet = new Wallet({ createAccessKeyFor: CONTRACT_ADDRESS });

await wallet.callMethod({
method: 'nft_transfer',
args: {
token_id: '1',
receiver_id: 'bob.near',
},
contractId: CONTRACT_ADDRESS,
deposit: 1,
});

The Wallet object comes from our quickstart template


Attaching NFTs to a Call​

Natively, only NEAR tokens (Ⓝ) can be attached to a function calls. However, the NFT standard enables to attach a non-fungible tokens in a call by using the NFT-contract as intermediary. This means that, instead of you attaching tokens directly to the call, you ask the NFT-contract to do both a transfer and a function call in your name.

near call <nft-contract> nft_transfer_call '{"receiver_id": "<receiver-contract>", "token_id": "<token_id>", "msg": "<a-string-message>"}' --accountId <your-account> --depositYocto 1
info

Optionally, a memo parameter can be passed to provide more information to your contract.


How Does it Work?​

Assume you want to attach an NFT (🎫) to a call on the receiver contract. The workflow is as follows:

  1. You call nft_transfer_call in the NFT-contract passing: the receiver, a message, and the token-id of 🎫.
  2. The NFT contract transfers the NFT 🎫 to the receiver.
  3. The NFT contract calls receiver.nft_on_transfer(sender, token-owner, token-id, msg).
  4. The NFT contract handles errors in the nft_resolve_transfer callback.
  5. The NFT contract returns true if it succeeded.

The nft_on_transfer method​

From the workflow above it follows that the receiver we want to call needs to implement the nft_on_transfer method. When executed, such method will know:

  • Who is sending the NFT, since it is a parameter
  • Who is the current owner, since it is a parameter
  • Which NFT was transferred, since it is a parameter.
  • If there are any parameters encoded as a message

The nft_on_transfer must return true if the NFT has to be returned to the sender.


Approving Users​

You can authorize other users to transfer an NFT you own. This is useful, for example, to enable listing your NFT in a marketplace. In such scenario, you trust that the marketplace will only transfer the NFT upon receiving a certain amount of money in exchange.

near call <nft-contract> nft_approve '{
"token_id": "<token-unique-id>",
"account_id": "<authorized-account>",
"msg": "<json-structure>"
}' --accountId <your-account> --depositYocto 1
info

If the msg parameter is included, then a cross-contract call will be made to <authorized_account>.nft_on_approve(msg). Which in turn will make a callback to nft_resolve_transfer in your NFT contract.


Tutorials​

  • NFT Tutorial Zero to Hero (JavaScript SDK) - a set of tutorials that cover how to create a NFT contract using JavaScript.
  • NFT Tutorial Zero to Hero (Rust SDK) - a set of tutorials that cover how to create a NFT contract using Rust.