How To Create An NFT On Bitcoin

DAMIAN ROBINSONDAMIAN ROBINSON
4 min read

Introduction

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.

Step 1: Define The NFT

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)

Create The Project

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

clarinet new sip009-nft

Add 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

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:

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

Implement The 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.

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 tx-senderto 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? stacksies 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? 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))

Wrap Up

That’s all there is to it! Basic NFTs in Clarity are really quite easy to do. Of course, this was just an introduction and things can get more complex, but I hope this helps any newbies out there. The full source code of the project can be found here.

0
Subscribe to my newsletter

Read articles from DAMIAN ROBINSON directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

DAMIAN ROBINSON
DAMIAN ROBINSON