Clarity Smart Contract Language

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 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

The nft-trait trait that we will implement has an official mainnet deployment address as detailed in the SIP document. Inside the sip009-nft project folder, we first specify a dependency on this trait, using Clarinet's requirements:

clarinet requirements add SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait

That command adds the following to Clarinet.toml:

[[project.requirements]]
contract_id = "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait"

Clarinet uses this information to download the contract from the network and takes care of deploying it to local or test networks for your testing.

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

clarinet contract new stacksies

Preparation work

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

(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

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

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

(define-non-fungible-token stacksies 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 '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 stacksies 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? stacksies token-id))
)

transfer

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

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

mint

We will also add a convenience function to mint new tokens. A simple guard to check if the contract-caller 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 contract-caller contract-owner) err-owner-only)
        (try! (nft-mint? stacksies 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? .stacksies mint tx-sender)
Events emitted
{"type":"nft_mint_event","nft_mint_event":{"asset_identifier":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.stacksies::stacksies","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? .stacksies transfer u1 tx-sender 'ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK)
Events emitted
{"type":"nft_transfer_event","nft_transfer_event":{"asset_identifier":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.stacksies::stacksies","sender":"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE","recipient":"ST1J4G6RR643BCG8G8SR6M2D9Z9KXT2NJDRK3FBTK","value":"u1"}}
(ok true)

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

>> (contract-call? .stacksies 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.