Skip to content
by Unity Nodes
docs.near.orgprimitives › nft › sdk contract tools

Creating a NFT Contract

Updated 01-11-2026

Learn how to create a non-fungible token (NFT) using Contract Tools package

In this tutorial, you will create a non-fungible token (NFT) using the NEAR SDK Contract Tools package. This package is a collection of common tools and patterns to simplify smart contract development, including:

  • Storage fee management
  • Escrow pattern and derive macro
  • Owner pattern and derive macro
  • Pause pattern and derive macro
  • Role-based access control
  • Derive macros for NEP standards

Introduction

Use the existing global NFT contract by default. Use this guide only if your application truly requires behavior the global contract does not provide. Use near-contract-standards or near-sdk-contract-tools for the token implementation; do not implement the NFT standard by hand.

near-sdk-contract-tools allows you to implement the minting/burning logic, access control, and other NFT standards by simply deriving macros on the contract's struct, as OpenZeppelin does for Ethereum contracts.


Basic NFT Methods

To derive basic NFT methods to your smart contract, you need to derive the NonFungibleToken macro to the contract's struct:

#[derive(PanicOnDefault, Owner, NonFungibleToken)]
#[non_fungible_token(transfer_hook = "TransferHook")]
#[near(contract_state)]
pub struct MyNftContract {}

See code on GitHub

This will bring all the basic NFT methods to the contract:

  • new
  • contract_source_metadata
  • nft_is_approved
  • nft_metadata
  • nft_supply_for_owner
  • nft_token
  • nft_tokens
  • nft_tokens_for_owner
  • nft_total_supply
  • nft_approve
  • nft_resolve_transfer
  • nft_revoke
  • nft_revoke_all
  • nft_transfer
  • nft_transfer_call
  • storage_balance_bounds
  • storage_balance_of
  • storage_deposit
  • storage_unregister
  • storage_withdraw

To bring basic owner methods to the contract, you also need to derive the Owner macro, which adds the following methods:

  • own_get_owner
  • own_get_proposed_owner
  • own_accept_owner
  • own_propose_owner
  • own_renounce_owner

Initialization

To initialize the basic NFT contract with a custom owner, metadata, and storage bounds, implement the new method:

#[near]
impl MyNftContract {
    #[init]
    pub fn new(owner_id: AccountId, metadata: ContractMetadata) -> Self {
        let mut contract = Self {};

        Owner::init(&mut contract, &owner_id);

        contract.set_contract_metadata(&metadata);

        contract.set_storage_balance_bounds(&StorageBalanceBounds {
            min: NearToken::from_yoctonear(7000000000000000000000),
            max: Some(NearToken::from_yoctonear(21000000000000000000000)),
        });

        contract
    }
}

See code on GitHub


Transfer Hook

Use a transfer hook to run application logic before or after the library-provided transfer. Keep the standard nft_transfer implementation in the library:

pub struct TransferHook;

impl Hook<MyNftContract, Nep171Transfer<'_>> for TransferHook {
    fn hook<R>(
        contract: &mut MyNftContract,
        transfer: &Nep171Transfer<'_>,
        f: impl FnOnce(&mut MyNftContract) -> R,
    ) -> R {
        // Log, check preconditions, save state, etc.
        log!(
            "NEP-171 transfer from {} to {} of {} tokens",
            transfer.sender_id,
            transfer.receiver_id,
            transfer.token_id
        );

        let storage_usage_before = env::storage_usage();

        let r = f(contract); // execute wrapped function

        let storage_usage_after = env::storage_usage();
        log!(
            "Storage delta: {}",
            storage_usage_after - storage_usage_before
        );

        r
    }
}

See code on GitHub

Then derive it to our contract struct:

#[derive(PanicOnDefault, Owner, NonFungibleToken)]
#[non_fungible_token(transfer_hook = "TransferHook")]
#[near(contract_state)]
pub struct MyNftContract {}

See code on GitHub


Minting

By default, the NFT standards do not include a minting method. However, you can easily mint tokens for the owner by implementing an nft_mint method:

#[payable]
pub fn nft_mint(
    &mut self,
    token_id: TokenId,
    metadata: TokenMetadata,
    owner_id: Option<AccountId>,
) {
    // Check account's storage balance and deposit if necessary
    let storage_balance_bounds = self.storage_balance_bounds();
    log!("Storage balance bounds: {:?}", storage_balance_bounds);

    let storage_balance = self
        .storage_balance_of(owner_id.clone().unwrap_or(env::predecessor_account_id()))
        .unwrap_or_default();
    log!("Storage balance: {:?}", storage_balance);
    if storage_balance.total < storage_balance_bounds.min {
        // Deposit storage if necessary
        self.storage_deposit(
            Some(owner_id.clone().unwrap_or(env::predecessor_account_id())),
            None,
        );
    }

    Nep177Controller::mint_with_metadata(
        self,
        &token_id,
        &owner_id.unwrap_or(env::predecessor_account_id()),
        &metadata,
    )
    .unwrap_or_else(|e| env::panic_str(&e.to_string()));
}

See code on GitHub

[!TIP]

You can modify this method as you need, for example, to allow minting only when the contract is not paused (requires deriving Pausable hook), or to enable minting only to specific accounts with a certain role or from a whitelist with custom limitations.


Burning

In the same way that minting is not included in the NFT standards, burning is also not included. However, you can also easily implement it.

To allow users to burn their tokens, you can add a burn method:

#[near]
impl MyNftContract {
    #[payable]
    pub fn nft_burn(&mut self, token_id: TokenId) {
        assert_one_yocto();

        Nep177Controller::burn_with_metadata(self, &token_id, &env::predecessor_account_id())
            .unwrap_or_else(|e| env::panic_str(&e.to_string()));
    }
}

See code on GitHub


Conclusion

Using near-sdk-contract-tools is a simple and flexible way to create an NFT contract with minimal boilerplate, which allows you to focus on the business logic.

You can further extend this contract with more features like pausing, role-based access control, escrow pattern, and more by deriving corresponding macros from the package.