Content

Clarity of Mind Foreword Introduction

Creating a SIP009 NFT

About time we create our own SIP009-compliant NFT! We will quickly go over the aforementioned built-in NFT functions to understand how they work and what their limitations are, and then use them to implement the SIP009 trait.

Built-in NFT functions

A new non-fungible token class is defined using the define-non-fungible-token function. NFTs have a unique asset name (per contract) and are individually identified by an asset identifier. The developer is free to choose the identifier type although an incrementing unsigned integer is most common.

(define-non-fungible-token asset-name asset-identifier-type)

The naming schedule is the same as those for variables or function names. The asset identifier type is a normal type signature. We can create an NFT class my-awesome token, identified by an unsigned integer as follows:

(define-non-fungible-token my-awesome-token uint)

One can then use the functions nft-mint?, nft-transfer?, nft-get-owner?, and nft-burn? to manage the NFT.

;; Define my-awesome-token
(define-non-fungible-token my-awesome-token uint)

;; Mint NFT with ID u1 and give it to tx-sender.
(nft-mint? my-awesome-token u1 tx-sender)

;; Transfer the NFT with ID u1 from tx-sender to another principal.
(nft-transfer? my-awesome-token u1 tx-sender 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK)

;; Get and print the new owner of the NFT with ID u1.
(print (nft-get-owner? my-awesome-token u1))

;; Burn the NFT with ID u1 (destroys it)
(nft-burn? my-awesome-token u1 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK)

Click the play button on the example above or copy it to the REPL to see the NFT events emitted for each function. Try defining and minting multiple NFTs, doing transfers and burns. Like the others, these functions are safe by design. It is not possible to transfer NFTs a principal does does not own, minting an NFT with the same ID twice, and burning tokens that do not exist.

Project setup

Let us create a new Clarinet project for our custom NFT contract.

clarinet new sip009-nft

Inside the sip009-nft project folder, we first create a new contract for the trait.

clarinet contract new sip009-nft-trait

Copy the SIP009 NFT trait to sip009-nft-trait.clar. You can delete the generated test file sip009-nft-trait_test.ts.

We then create the contract that will implement our custom NFT. Give it a flashy name if you like.

clarinet contract new marvin-token

Finally, we specify that our custom token contracts depends on the trait contract by editing Clarinet.toml. Find the entry for your contract and add the name of the trait contract in the depends_on property.

[contracts.marvin-token]
path = "contracts/marvin-token.clar"
depends_on = ["sip009-nft-trait"]

Preparation work

We have dealt with traits before, so we know that we should explicitly assert conformity.

(impl-trait .sip009-nft-trait.sip009-nft-trait)

Adding this line makes it impossible to deploy the contract if it does not fully implement the SIP009 trait.

The trait also has an official mainnet deployment address as detailed in the SIP document. We can refer to it when we deploy but Clarinet cannot currently deal with such external references. We will add it as a comment for future use.

;; SIP009 NFT trait on mainnet
;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

Since the SIP requires the asset identifier type to be an unsigned integer, we add our NFT definition next.

(define-non-fungible-token marvin-token uint)

The asset identifier should be an incrementing unsigned integer. The easiest way to implement it is to increment a counter variable each time a new NFT is minted. Let us define a data variable for it.

(define-data-var last-token-id uint u0)

Finally, we will add a constant for the contract deployer and two error codes. Here is everything put together:

(impl-trait .sip009-nft-trait.nft-trait)

;; SIP009 NFT trait on mainnet
;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-token-owner (err u101))

(define-non-fungible-token marvin-token uint)

(define-data-var last-token-id uint u0)

Implementing the SIP009 NFT trait

The implementation is rather simple, thanks to the built-in NFT functionality.

get-last-token-id

We use a variable to track the last token ID.

(define-read-only (get-last-token-id)
    (ok (var-get last-token-id))
)

get-token-uri

The idea of get-token-uri is to return a link to metadata for the specified NFT. Our practice NFT does not have a website so we can return none.

(define-read-only (get-token-uri (token-id uint))
    (ok none)
)

However, even if we did have a website, there are a few challenges to implementing this seemingly straightforward function. Usually, these functions return a base URL with the token ID stuck behind it. The current version of Clarity (2.0) does not feature a intuitive way to do this. Something like the following is impossible because there is no to-ascii function to turn a number into an ASCII string type.

(concat "https://domain.tld/metadata/" (to-ascii token-id))

It does not mean that turning a number into a string cannot be done. It is just way more strenuous than it should be. Clarity 2.1 will solve the issue.

get-owner

The get-owner function only has to wrap the built-in nft-get-owner?.

(define-read-only (get-owner (token-id uint))
    (ok (nft-get-owner? marvin-token token-id))
)

transfer

The transfer function should assert that the sender is equal to the tx-sender to prevent principals from transferring tokens they do not own.

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender sender) err-not-token-owner)
        (nft-transfer? marvin-token token-id sender recipient)
    )
)

mint

We will also add a convenience function to mint new tokens. A simple guard to check if the tx-sender is equal to the contract-owner constant will prevent others from minting new tokens. The function will increment the last token ID and then mint a new token for the recipient.

(define-public (mint (recipient principal))
    (let
        (
            (token-id (+ (var-get last-token-id) u1))
        )
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (try! (nft-mint? marvin-token token-id recipient))
        (var-set last-token-id token-id)
        (ok token-id)
    )
)

Manual testing

Check if the contract conforms to SIP009 with clarinet check. We then enter a console session clarinet console and try to mint a token for ourselves.

>> (contract-call? .marvin-token mint tx-sender)
Events emitted
{"type":"nft_mint_event","nft_mint_event":{"asset_identifier":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.marvin-token::marvin-token","recipient":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE","value":"u1"}}
(ok u1)

You can see the NFT mint event and the resulting ok response. We can transfer the newly minted token with ID u1 to a different principal.

>> (contract-call? .marvin-token transfer u1 tx-sender 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK)
Events emitted
{"type":"nft_transfer_event","nft_transfer_event":{"asset_identifier":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.marvin-token::marvin-token","sender":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE","recipient":"ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK","value":"u1"}}
(ok true)

get-owner confirms that the token is now owned by the specified principal.

>> (contract-call? .marvin-token get-owner u1)
(ok (some ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK))

That is all there is to it! NFTs in Clarity are really quite easy to do. The full source code of the project can be found here: https://github.com/clarity-lang/book/tree/main/projects/sip009-nft.