Creating an NFT Auction Dapp With Clarity and Stacks.js: Step-by-Step Guide
In this tutorial, we would be learning how to work with Clarity to write smart contracts on the Stacks blockchain and connect it to a next.js app using Stacks.js and we want to achieve that using an NFT Auction dapp as a sample project. You can check out the github repo anytime if you want to compare your code along the way.
Assumption
Since this is not a 101 beginner guide, it is assumed that you already successfully installed Clarinet on your machine and that it is available in your system PATH (that is to say, typing clarinet in your Terminal runs Clarinet), if you have not, you can get helpful guide from Hiro's website.
Overview
Auctions have a long-standing history spanning back to centuries. It is interesting to see how this method of fundraising has evolved over the years. Today, bidding is one of the fastest-growing use cases on the blockchain. All the credits go to decentralized NFT marketplaces that allow buyers and sellers to engage in a peer-to-peer model. That is why in this tutorial, we will build an NFT Auction Dapp which we would call NFT Auction and it will have the following features
A user can whitelist their NFT and create an auction for it.
Once an auction is started, the auction maker (maker) can not withdraw their NFT.
Every auction will take an expiry time in
block height
which would correspond to the time the auction will end or expireThe maker will only get paid in STX
The bidder can only place a bid if they bid higher than the existing highest bid
If a bidder did not win the bid, they can request a refund on the bid amount
Once a bidder places a bid, they can not withdraw their bid until the auction ends
Once the bid ends, the winner can claim their NFT and the maker their STX
Workflow
Our project would have a backend and a frontend.
The backend would hold all our smart contracts and everything associated with it, and the frontend would hold everything that the user would use to interact with the smart contract.
Backend
To create an auction, the maker would call the create-auction
function from the auction contract with the following arguments
The contract of the NFT to be listed
The block height when the auction expires
The start price for the auction
and token id of the NFT
Once the call is made, the NFT is transferred into the contract which serves as an escrow and is kept there till the winner of the bid emerges. Once an auction is started, it would not be stopped halfway.
To place-a-bid, the bidder would call the place-a-bid
function from the auction contract with the following arguments
The contract of the NFT they want to bid for
The auction id of the auction
The amount of STX they are biding
and token id of the NFT
A bidder can bid for an NFT they like. Once a bid is placed, the STX is transferred into the contract, which serves as our escrow. If the bidder wins, they can claim the NFT at the expiry of the bid period. A bidder can withdraw their bid if at the expiry of the bid period they do not win the bid but they can not change their mind halfway through the bid and take their bid from the contract.
Setup
To start, let's head over to our terminal window and type in this command bash clarinet new NFTAuction
The above line would create a clarity project folder for us and some other important folders we would need for the project.
Let's now create a contract called auction by running the following commands in our terminal window
bash cd NFTAuction mkdir backend cd backend
clarinet contract new auction
This creates an auction.clar
file in the contract folder. If we go into our NFTAuction/backend/contracts
we would see
NFT Contract
Our auction dapp is for NFTs, so for this tutorial, we will create a simple NFT implementing the sip009 trait. Traits are similar to token standards if you are coming from Ethereum, they are just a set of publicly defined functions that are used to define input types, output types and names.
IMPLEMENTING SIP009 TRAIT
To create an NFT implementing the sip009 trait, we would create a new contract and call it sip009
. Point your terminal to the NFTAuction directory and run the command to create a contract again clarinet contract new sip009
Yes, you are right! This would add the sip009.clar
file to the contract folder. Next, we would import the trait that we are implementing.
clarinet requirements add SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait
The command line above adds [[project.requirements]] contract_id = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait'
to our Clarinet.toml
file which allows us to tap into the sip009-traits
.
With that done, let us start building 🛠
At the top of your file, let us assert conformity with our sip009 trait which simply means we implementing the specifications of the sip009 contract - that is the functions defined in the trait. On the topmost of our contract, we would add the line
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)(define-constant contract-owner tx-sender)
The first line indicates that we are implementing the sip009-nft
trait. The next line sets the contract owner to be the one who deploys the contract. That means whoever mints the NFT becomes the owner of the NFT, which makes sense right?, yes that is the security built into Clarity.
Let us define errors for the contract, and put these lines next, they simply set errors to err codes Clarity (define-constant err-only-owner (err u100)) (define-constant err-token-id-failure (err u101)) (define-constant err-not-token-owner (err u102))
To create our NFT, we would use
(define-non-fungible-token auctionnfts uint)
(define-data-var token-id-nonce uint u0)
The line creates an NFT collection with the name auctionnfts
.
The next line defines a variable token-id-nonce
of type, uint starting with the value of 0 (Note it is written as u0 because it is an unsigned integer) that sets a unique id for each NFT minted.
To implement the NFT trait, what we need to do is simply define all the required functions defined in the [official deployed contract](https://explorer.stacks.co/txid/0x80eb693e5e2a9928094792080b7f6d69d66ea9cc881bc465e8d9c5c621bd4d07?chain=mainnet)
, so we would start with the get-last-token-id
.
(define-read-only (get-last-token-id)
(ok (var-get token-id-nonce) )
)
The line above defines a read-only function, which is a function that does not modify the state of the blockchain, (so it only reads from the blockchain) called get-last-token-id
and returns the token id
as an okay response.
(define-read-only (get-token-uri (token-id uint))
(ok none)
)
Next, we define another read-only function to get token-uri
. The function takes in a token-id as a parameter and returns the token-URI.
(define-read-only (get-owner (token-id uint))
(ok (nft-get-owner? auctionnfts token-id))
)
After that we define yet another read-only function to check for the owner of the token, it is the token-id and returns the principal that owns the token
(define-public (transfer (token-id uint ) (sender principal) (reciever principal))
(begin
(asserts! (is-eq tx-sender sender) err-not-token-owner)
;; #[filter(token-id, reciever)]
(nft-transfer? auctionnfts token-id sender reciever)
)
)
)
Now let's define a transfer function. It would be a public function meaning it can be called from outside the contract by maybe an app or another contract, it would take the receiver's principal, the sender's principal and the token-id and the body of the function to perform a couple of checks, the first check is to ensure that the sender is the owner of the token, if true, then the second line transfers the token from the sender to the receiver.
(define-public (mint ( reciever principal))
(let
(
(token-id (+ (var-get token-id-nonce) u1 ) )
)
(asserts! (is-eq tx-sender reciever) err-not-token-owner)
(try! (nft-mint? auctionnfts token-id reciever))
(asserts! (var-set token-id-nonce token-id) err-token-id-failure)
(ok token-id)
)
)
Finally, we would define a mint function which is also a public function that allows for external calls. The function takes the receiver's principal - that is the principal that is calling the mint function. Once the call is made, first, the function sets the token id to be the last token id + 1 then, It now checks to ensure that the principal calling the contract is the receiver of the token that would be minted After that, the next line, try to mint the token, if the token-id already exists, the operation fails and every state mutation gets reverted, otherwise, the last line runs and sets the token-nonce which is the specific identifier given to the token when created to be the value of the token-id and then returns the token id of the minted token.
By now your sip009.clar
file should look like this
(define-non-fungible-token auctionnfts uint)
(define-data-var token-id-nonce uint u0)
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
(define-constant contract-owner tx-sender)
;; NFT errors
(define-constant err-only-owner (err u100))
(define-constant err-token-id-failure (err u101))
(define-constant err-not-token-owner (err u102))
;; create the NFT collection
(define-non-fungible-token auctionnfts uint)
(define-data-var token-id-nonce uint u0)
;; function to get the last token id
(define-read-only (get-last-token-id)
(ok (var-get token-id-nonce) )
)
;; function to get token URI
(define-read-only (get-token-uri (token-id uint))
(ok none)
)
;; function to fetch owner of the NFT
(define-read-only (get-owner (token-id uint))
(ok (nft-get-owner? auctionnfts token-id))
)
;; function for NFT transfer
(define-public (transfer (token-id uint ) (sender principal) (reciever principal))
(begin
(asserts! (is-eq tx-sender sender) err-not-token-owner)
;; #[filter(token-id, reciever)]
(nft-transfer? auctionnfts token-id sender reciever)
)
)
;; funton to mint NFT
(define-public (mint ( reciever principal))
(let
(
(token-id (+ (var-get token-id-nonce) u1 ) )
)
(asserts! (is-eq tx-sender reciever) err-not-token-owner)
(try! (nft-mint? auctionnfts token-id reciever))
(asserts! (var-set token-id-nonce token-id) err-token-id-failure)
(ok token-id)
)
)
The Auction Contract
Our auction contract, at its most basic, is an escrow that takes an NFT when an auction is created. We would be using the NFT we just created for testing later.
Now let's start working on the NFTAuction. Let's go to our auction.clar
that we created earlier when we started and import traits we would be needing and define our constants.
In our terminal window, we would run
clarinet requirements add SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait
which would import the trait requirements for using SIP009 and add [[project.requirements]] contract_id = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait'
them to our Clarinet.toml
file. Next, we would define constants for our project. Let's start with,
;; auction-creation errors
(define-constant err-expiry-not-set (err u1000))
(define-constant err-start-price-not-set (err u1001))
(define-constant err-asset-contract-not-whitelisted (err u1002))
(define-constant err-invalid-auction (err u1003))
(define-constant err-unauthorized (err u1004))
;; bidding errors
(define-constant err-token-invalid (err u2000))
(define-constant err-bid-amount-too-low (err u2001))
(define-constant err-maker-taker-equal (err u2002))
(define-constant err-already-expired (err u2003))
(define-constant err-invalid-price (err u2004))
(define-constant err-no-such-bid (err u2005))
;; settlement error
(define-constant err-invalid-settlement (err u3001))
(define-constant err-expiry-not-reached-yet (err u3002))
(define-constant err-invalid-bider (err u3003))
(define-constant err-not-bid-winner (err u3004))
;; withdrawer error
(define-constant err-bid-winner-cannot-withdraw (err u4001))
The NFTAuction place would have to store some data about the auctions created, for example, every auction created should have a unique id, tracked by an unsigned integer (uint) that increments for each auction created and never re-used. This would be stored in a map
This is what our setup would look like
;; auction
;; <add a description here>
;; use the nft-trait imported for use in the dapp
(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
;; set the address of the contract deployer to be the contract-owner
(define-constant contract-owner tx-sender)
;; auction-creation errors
(define-constant err-expiry-not-set (err u1000))
(define-constant err-start-price-not-set (err u1001))
(define-constant err-asset-contract-not-whitelisted (err u1002))
(define-constant err-invalid-auction (err u1003))
(define-constant err-unauthorized (err u1004))
;; biding errors
(define-constant err-token-invalid (err u2000))
(define-constant err-bid-amount-too-low (err u2001))
(define-constant err-maker-taker-equal (err u2002))
(define-constant err-already-expired (err u2003))
(define-constant err-invalid-price (err u2004))
(define-constant err-no-such-bid (err u2005))
;; settlement error
(define-constant err-invalid-settlement (err u3001))
(define-constant err-expiry-not-reached-yet (err u3002))
(define-constant err-invalid-bider (err u3003))
(define-constant err-not-bid-winner (err u3004))
;; withdrawer error
(define-constant err-bid-winner-cannot-take-refund (err u4001))
;; map for auctions - basically the details of an auction
;; 'auctions' maps a uint(auction-id) to a tuple of
;; - the auction maker
;; - token-id of the NFT placed on auction
;; - the assets-contract-principal
;; - the expiry block height
;; - the start-price for the auction
(define-map auctions
uint {
maker: principal,
token-id: uint,
nft-asset-contract: principal,
expiry: uint,
start-price: uint,
}
)
;; count to identify auction
(define-data-var auction-nonce uint u0)
;; map for bids
;; maps a principal to the total amount that principal have bided
(define-map bids principal {
bider: principal,
biders-total-bid: uint,
})
;; get a bider bid using the principal
(define-read-only (get-bid (who principal))
(map-get? bids who)
)
;; count to identify bids
(define-data-var bid-nonce uint u0)
;; variable to track highest-bid-amount
(define-data-var highest-bid-amount uint u250 )
;; variable to hold the highest-bider principal
(define-data-var highest-bider principal contract-owner )
;; variable to hold a bider's total bid
(define-data-var biders-total-bid uint u0)
;; keep tract of all bids
(define-data-var total-bids uint u0)
;; fetch the total bid value
(define-read-only (get-total-bids)
(var-get total-bids)
)
(define-read-only (get-biders-total-bid)
(var-get biders-total-bid)
)
;; get the highest-bid-amount
(define-read-only (get-highest-bid-amount)
(var-get highest-bid-amount)
)
;; get the highest-bider principal
(define-read-only (get-highest-bider)
(var-get highest-bider)
)
;; map of whitelisted assets to their principals
;; if an asset is whitelised, it returns true,
;; and false otherwise
(define-map whitelisted-assets-contract principal bool)
;; fetch whitelisted asset-contract from the whitelisted-contract-asset map
(define-read-only (is-whitelisted (asset-contract principal))
(default-to false (map-get? whitelisted-assets-contract asset-contract) )
)
;; update the whitelisted-contract-asset map
(define-public (set-whitelisted (asset-contract principal) (whitelisted bool))
(begin
(asserts! (is-eq contract-owner tx-sender ) err-unauthorized)
;; #[filter(asset-contract)]
(ok (map-set whitelisted-assets-contract asset-contract whitelisted))
)
)
;; helper function to transfer nft
(define-private (transfer-nft (token-contract <nft-trait>) (token-id uint) (sender principal) (reciever principal))
(contract-call? token-contract transfer token-id sender reciever)
)
;; private functions
;;
;; public functions
;;
Before any auction can be created, the NFT contract MUST be whitelisted first. Then and only then would the create-auction
call run without error.
Starting an auction
;; function for creating an auction
;; create-auction, take variables:-
;; - nft-trait
;; - token-id
;; - expiry
;; - start-price
(define-public (create-auction (nft-asset-contract <nft-trait>) (nft-asset { token-id: uint, expiry: uint, start-price: uint, }))
(let (
;; take auction-nonce and call it auction-id
;; and use it in the within the `create-auction`
;; code block
(auction-id (var-get auction-nonce))
)
;; check if NFT is whitelisted
(asserts! (is-whitelisted (contract-of nft-asset-contract)) err-asset-contract-not-whitelisted)
;; check if the duration has expired
(asserts! (> (get expiry nft-asset) block-height ) err-expiry-not-set)
;; check if the start price for the auction is set
(asserts! (> (get start-price nft-asset) u0) err-start-price-not-set)
;; transfer NFT to the contract
(try! (transfer-nft nft-asset-contract (get token-id nft-asset) tx-sender (as-contract tx-sender)) )
;; set highest-bid to start-price specified by the maker
(var-set highest-bid-amount (get start-price nft-asset) )
;; update the total bids vriable
(var-set total-bids (+ (get start-price nft-asset) (var-get total-bids)) )
;; add auction to the auctions map
(map-set auctions auction-id (merge {maker: tx-sender, nft-asset-contract: (contract-of nft-asset-contract)} nft-asset) )
;; increament the auction identifier
(var-set auction-nonce (+ auction-id u1))
;; return the auction id
(ok auction-id)
)
)
;; can be used to fetch auction from the auction map using auction-id
(define-read-only (get-auction (auction-id uint))
(map-get? auctions auction-id)
)
We have successfully created create-auction
feature for our dapp, you can test it if you want by heading over to your terminal and pointing to NFTAuction/backend
and typing the following
clarinet console
deploy our contract locally and spin up a bunch of test accounts with 100M STX for testing, NOTE: the first account would be the deployer account.
below the array of accounts, type to mint and 'auctionnft' (contract-call? .sip009 mint tx-sender)
This would call the mint function from the sip009 contract that we deployed and mint it to the caller of the contract you would get a response Events emitted {"type":"nft_mint_event","nft_mint_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009::auctionnfts","recipient":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","value":"u1"}} (ok u1)
to indicate that the call was successful.
Now you can run the command
(contract-call? .auction set-whitelisted .sip009 true)
You would get a response (ok "NFT whitelisted successfully")
to indicate that the call was successful.
Then create-auction
(contract-call? .auction create-auction .sip009 {token-id: u1, expiry: u10, start-price: u1000})
You would get a response
Events emitted {"type":"nft_transfer_event","nft_transfer_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009::auctionnfts","sender":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","recipient":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction","value":"u1"}} (ok u0)
to indicate that the call was successful.
Placing a bid
To place a bid, the bidder would need to call the place-a-bid
function and pass in the following parameters:-
bid amount
token id
NFT contract identifier
and auction id
(define-public (place-a-bid (nft-asset-contract <nft-trait>) (bid-details {token-id: uint, bid-amount: uint, auction-id: uint}) )
(let (
;; use the auction-id passed when making the call to
;; create a tuple called `auction` to be used
;; throughout the place-a-bid fuction
(auction (unwrap! (map-get? auctions (get auction-id bid-details)) err-invalid-auction) )
;; use the bid-id passed when making the call to
;; set a new bid-id that would become the bid-id
;; of this very bid
(bid-id (+ (var-get bid-nonce) u1))
;; fetch the highest bid in the auction and name it
;; highest-bid
(highest-bid (var-get highest-bid-amount))
)
;; check if maker is the one biding
;; remeber we said the maker can not bid with thesame
;; principal they used to create the auction
(asserts! (not (is-eq tx-sender (get maker auction))) err-maker-taker-equal)
;; check if the NFT is actually put on auction/whitelisted
(asserts! (is-whitelisted (contract-of nft-asset-contract)) err-asset-contract-not-whitelisted)
;; check if the bid duration has expired
(asserts! (> (get expiry auction) block-height) err-already-expired)
;; make sure that bid-amount > hgiest-bid
(asserts! (> (get bid-amount bid-details) highest-bid ) err-bid-amount-too-low)
;; check if the STX transfered > highest-bid-amount
(try! (stx-transfer? (get bid-amount bid-details) tx-sender (as-contract tx-sender)) )
;; set highest-bid-amount to bid-amount
(var-set highest-bid-amount (get bid-amount bid-details))
;; set highest-bider to tx-sender
(var-set highest-bider tx-sender)
;; update the total bids
(var-set total-bids (+ (get bid-amount bid-details) (var-get total-bids)) )
;; update the bids map with the caller's bid-amount
;; using thier principal as the key
(map-set bids tx-sender { bider: tx-sender, biders-total-bid: (get bid-amount bid-details)} )
;; return the auction id
(ok tx-sender)
)
we can test to see if our place-a-bid
function is also functioning properly, below the create-auction
call that we made, we would run the following command. Note that we said the maker can not use the same principal to place-a-bid
so let's first switch our 'tx-sender' by running
::set_tx_sender ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
You would get a response tx-sender switched to ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
to indicate that the call was successful.
(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction place-a-bid 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009 {token-id: u1, auction-id: u0, bid-amount: u2000})
Be very mindful of the '
before the principal, that is how we indicate a standard principal. Also, notice that the 'bid-amount' is greater than the 'start-price', IT MUST HAVE TO BE, otherwise it would return an error because we specified that the 'bid-amount' must be more than the present highest-bid and we set the 'start-price' to be the 'highest-bid' when we first created the auction. Finally, note that when calling a contract function from the principal who deployed it, you need not write the identifier in full, hence we used '.auction' in our create-auction
call but when we call place-a-bid
using another principal, we would need to make it complete so we use 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction
and 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009
You would get a response
Events emitted {"type":"stx_transfer_event","stx_transfer_event":{"sender":"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC","recipient":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction","amount":"2000","memo":""}} (ok ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)
to indicate that the call was successful.
Repeat the process for two or three more principal so you can have a couple of bids to try with when you want to 'settle-auction' and 'request-refund'
Bid fulfilment
Once the bid time is up, the highest bidder can claim their NFT by providing
the contract identifier of the NFT they won
the auction id of the auction the NFT was listed in
(define-public (settle-auction (nft-asset-contract <nft-trait>) (auction-id uint) )
(let
(
;; use the auction-id passed when making the call to
;; create a tuple called `auction` to be used
;; throughout the settle-auction fuction
(auction (unwrap! (map-get? auctions auction-id ) err-invalid-auction))
;; use the tx-sender making the call to
;; create a tuple called `bid` that taps into the 'bids' map
;; which would be used throughout the settle-auction fuction
(bid (unwrap! (map-get? bids tx-sender) err-invalid-bider))
;; set the tx-sender to be called 'taker' throughout
;; the settle-auction fuction
;; (taker tx-sender)
)
;; check if aucton have expired
(asserts! (> block-height (get expiry auction)) err-expiry-not-reached-yet)
;; check if the caller is the bid winner
(asserts! (is-eq (get biders-total-bid bid) (var-get highest-bid-amount)) err-not-bid-winner)
;; send NFT from contract to highest bider
;; #[filter(nft-asset-contract)]
(try! (as-contract (transfer-nft nft-asset-contract (get token-id auction) tx-sender (var-get highest-bider) ) ))
;; send STX from contract to the maker
(try! (as-contract (stx-transfer? (var-get highest-bid-amount) tx-sender (get maker auction))))
(ok tx-sender)
)
)
You can test your settle-auction
feature by running some terminal commands as we have done in the previous tests
::get_assets_maps
this would give us a view of the assets and their owners distributed in our local deployment. From here, we can see the principals that bid for the auction as well as who has the NFT right now
Before we run settle-auction, we need to make sure that the contract has expired, in our local deployment, the block-hieght
does not increase, so we would need to move it manually, run the code ::advance_chain_tip 10
to move the block-height
to 11, If you did not run the above command, you would get an error return (err u3002). Then run the command.
(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction settle-auction 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009 u0)
You would get a response
Events emitted {"type":"nft_transfer_event","nft_transfer_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009::auctionnfts","sender":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction","recipient":"ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB","value":"u1"}} {"type":"stx_transfer_event","stx_transfer_event":{"sender":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction","recipient":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","amount":"3500","memo":""}} (ok ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB)
to indicate that the call was successful.
If we run now
::get_assets_maps
we would see that the amount of STX that was in the contract is less by the 'highest-bid-amount' now and the NFT has been transferred to the corresponding 'highest-bidder, all that is left is the total sum of STX of the other bidders who did not win
Request Refund
If by the end of the auction duration, a bidder does not win the bid, they can 'request-refund' on their bid amount by providing
the contract identifier of the NFT they placed a bid on
the auction id of the auction the NFT was listed in
;; If a bidder did not win the bid, they can request a refund on the bid amount
(define-public (request-refund (nft-asset-contract <nft-trait>) (auction-id uint))
(let
(
;; use the tx-sender making the call to
;; create a tuple called `bid` that taps into the 'bids' map
;; which would be used throughout the request-refund fuction
(bid (unwrap! (map-get? bids tx-sender ) err-invalid-bider))
;; use the auction-id passed when making the call to
;; create a tuple called `auction` to be used
;; throughout the request-refund fuction
(auction (unwrap! (map-get? auctions auction-id ) err-invalid-auction))
;; set the tx-sender to be called 'taker' throughout
;; the request-refund fuction
(taker tx-sender)
)
;; check that block-height > expiry
(asserts! (> block-height (get expiry auction)) err-expiry-not-reached-yet)
;; check that the caller is not the bid-winner
;; that is total-bid-amount < highest-bid amount
(asserts! (< (get biders-total-bid bid) (var-get highest-bid-amount)) err-bid-winner-cannot-take-refund)
;; transfer the biders amount back to them
(try! (as-contract (stx-transfer? (get biders-total-bid bid) tx-sender taker)))
(ok true)
)
)
::set_tx_sender <any-other-bider-other-than-the-bid-winner>
Switch principal to any other principal that placed a bid but was not the winner
(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction request-refund 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sip009 u0)
You would get a response Events emitted {"type":"stx_transfer_event","stx_transfer_event":{"sender":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.auction","recipient":"ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND","amount":"3000","memo":""}} (ok "STX refunded")
to indicate that the call was successful.
if we do a ::get_assets_maps
now, we would see that the STX sum in the contract is less by the caller's bid and the caller's bid has been refunded.
Congratulations, you have successfully created the NFTAuction place smart contract using Clarity and clarinet.
FRONTEND
Now that we are done with the smart contract, we would hook it to a next.js frontend using Stacks.js which is a full fledged js library for dapps on stacks blockchain.
To build our frontend and test it, we would require a couple of things
Requirements
You would need to have npm
running on you machine, I am using v 9.1.3
if you are not sure what you are using, run
npm -v
outside your clarinet console to see your npm
version.
You would be using Docker to run a local devnet on your machine, you can install one here if you do not already have.
Since this is not a react-next or tailwind tutorial, we would just supply a boiler plate code for the next app and focus more on the stack.js side of things.
Back to our NFTAuction
create a folder called frontend
using the code. ( Assuming you have been following from the begining non stop, otherwise just point terminal to your project root directory )
cd ../
mkdir frontend
cd frontend
in the frontend folder, run
npm create-next-app@latest
to create a next app for you and in the same folder. Your folder structure at this point should look like this shell NFTAuctionDapp -backend -frontend
Now run
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
The first line would install tailwindcss, postcss and autoprefixer as dev dependency in our frontend folder, the second line would create a tailwind.config.js file where we can configure our tailwindcss for the app. Now let's go in to the tailwind.config.js and add some codes to content as follows to allow tailwind to access our pages and components(which we would create in a sec) folders
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
And now let's add tailwind to our global styles so that we can see and use it from anywhere within the app, let's go to global.css in our styles folder under the frontend folder and paste the following lines, replacing all that is there
@tailwind base;
@tailwind components;
@tailwind utilities;
with this, we are all set to build our frontend of the dapp.
PAGES
Now you can replace the code in your index.js file with
Index.js page
import NavBar from '../components/NavBar'
import MintNFT from '../components/MintSIP009';
export default function Home() {
return (
<div>
<NavBar />
<div>
<h2 className='font-bold py-8 px-4 text-2xl'>
The NFT Place
</h2>
<p className='px-4 text-lg '>
Welcome to the NFT Auction Place,
you are welcome to create an Auction or place a bid
just go ahead and connect your wallet and you are all set to roll
</p>
<h2 className='font-bold py-4 px-4 text-2xl'>
Mint a token and try the platform
</h2>
</div >
<div className = ' px-4 '>
< MintNFT />
</div>
</div>
)
}
What is going on in this index.js file? Nothing gang-aang at all (It means nothing serious at all) Just a regular component that retuns some texts to welcome our user and a MintNFT component to allow user mint a sample auctionnft that they can use to test the platform.
auction.js page
import NavBar from '../components/NavBar'
import { Connect, } from "@stacks/connect-react";
import {
AppConfig,
UserSession,
} from "@stacks/connect";
import WhitelistNFT from '../components/Whitelist';
import CreateAuction from '../components/CreateAuction';
export default function Home() {
const appConfig = new AppConfig(["publish_data"]);
const userSession = new UserSession({ appConfig });
const authOption = {
appDetails: {
name: 'auction',
icon: 'https://assets.website-files.com/618b0aafa4afde65f2fe38fe/618b0aafa4afde2ae1fe3a1f_icon-isotipo.svg',
},
redirectTo: '/',
userSession: userSession,
}
return (
<div>
<NavBar />
<div className='py-16 '>
<div>
<h2 className='px-4 pb-6 font-bold text-xl'>
Create an Auction, it is our pleasure to allow you liquidate your NFT
</h2>
<p className='px-4 pb-4'>
All you need to do is first whitelist your NFT,
Then you can put it on auction!!
</p>
</div>
{/*
implimentation of whitelist
*/}
<Connect authOptions={authOption}>
< WhitelistNFT />
</Connect>
<div>
<h2 className='px-4 pb-6 font-bold text-xl'>
You can now put your NFT on auction!!
</h2>
<p className='px-4 pb-4'>
Provide the NFT identifier, its token id, an auction starting price
and then set a duration for your auction.
</p>
</div>
{/*
implimentation of create auction
*/}
<Connect authOptions={authOption}>
< CreateAuction />
</Connect>
</div>
</div>
)
}
bid.js page
This would be the bids page
import NavBar from '../components/NavBar'
import {
AppConfig,
UserSession,
} from "@stacks/connect";
import { useState, useEffect } from 'react';
import PlaceBid from '../components/PlaceABid';
import { Connect, } from "@stacks/connect-react";
export default function Home() {
const appConfig = new AppConfig(["publish_data"]);
const userSession = new UserSession({ appConfig });
const [userData, setUserData] = useState({})
const [loggedIn, setLoggedIn] = useState(false);
const authOption = {
appDetails: {
name: 'auction',
icon: 'https://assets.website-files.com/618b0aafa4afde65f2fe38fe/618b0aafa4afde2ae1fe3a1f_icon-isotipo.svg',
},
redirectTo: '/',
userSession: userSession,
}
useEffect(() => {
if (userSession.isSignInPending()) {
userSession.handlePendingSignIn().then((userData) => {
setUserData(userData)
})
} else if (userSession.isUserSignedIn()) {
setLoggedIn(true)
setUserData(userSession.loadUserData())
}
}, [])
return (
<div>
<NavBar />
<div className='py-16 '>
<div>
<h2 className='px-4 pb-6 font-bold text-xl'>
Place a bid, the highest bidder gets the NFT
</h2>
<p className='px-4 pb-4'>
Note: Once you place a bid, you can not widraw until the auction is over. <br/>
However you can request a refund if you did not win the bid
</p>
</div>
{/*
implimentation of whitelist
*/}
<Connect authOptions={authOption}>
< PlaceBid />
</Connect>
</div>
</div>
)
}
Now let's create a components folder inside our fronend folder inside which we would create NavBar.js and paste the following codes.
If you try running npm run dev
it would yell at you about the components not being
created, so we would create the components.
Note: If you run npm run dev
later and get an error about not being able to find the regenerator-runtime
package, run npm i regenerator-runtime
and it should be fixed.
NavBar.js component
import Link from 'next/link'
import ConnectWallet from './ConnectWallet'
const NavBar = () => {
return (
(<div className= 'p-10 flex flex-nowrap space-x-4 drop-shadow-lg bg-slate-100 h-10 justify-around ' >
<div>NFT Auction Place</div>
<div className='px-10 flex flex-row justify-around space-x-6'>
<div className='hover:underline '>
<Link href = {'/'}>
Home
</Link>
</div>
<div className='hover:underline '>
<Link href = {'/auction'}>
Create Auction
</Link>
</div>
<div className='hover:underline '>
<Link href = {'/bid'}>
Bid
</Link>
</div>
</div>
<div>
<ConnectWallet />
</div>
</div>)
);
}
export default NavBar;
Here also nothing gang-aang is happening ( which means nothing serious is happening), it is just a regular component that returns some clickable texts in the navbar and a button that allows users to connect and move around the platform easily.
ConnectWallet.js component
import React, { useEffect, useState } from "react";
import { AppConfig, showConnect, UserSession } from "@stacks/connect";
const appConfig = new AppConfig(["publish_data"]);
export const userSession = new UserSession({ appConfig });
// function for authenticating user
function authenticate() {
showConnect({
appDetails: {
name: "NFT Auction Place",
icon: window.location.origin + "/logo512.png",
},
redirectTo: "/",
onFinish: () => {
window.location.reload();
},
userSession,
});
}
// function to diconnect wallet
function disconnect() {
userSession.signUserOut("/");
}
// function for connecting wallet
const ConnectWallet = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (mounted && userSession.isUserSignedIn()) {
return (
<div>
<button className="
bg-white
hover:bg-gray-100
px-4
py-1
border
border-gray-600
text-gray-400
hover:text-gray-800
shadow
justify-start
mb-4
rounded-full"
onClick={disconnect}>
Disconnect Wallet
</button>
</div>
);
}
return (
<button className=" bg-white hover:bg-gray-100 px-4 py-1 border border-gray-600 text-gray-400 hover:text-gray-800 shadow justify-start mb-4 rounded-full" onClick={authenticate}>
Connect Wallet
</button>
);
};
export default ConnectWallet;
What is going on in the connectWallet component? Well, it is basically just authenticating the user using @stacks/connect
. If the user is not logged in, they get a pop-up to log into their web wallet or download one, if logged in, they also have a chance to disconnect if they so choose.
MintSIP009.js component
import { AppConfig, UserSession } from '@stacks/connect';
import { StacksMocknet } from '@stacks/network';
import {
NonFungibleConditionCode,
createAssetInfo,
makeStandardNonFungiblePostCondition,
bufferCVFromString,
standardPrincipalCV
} from '@stacks/transactions'
import {openContractCall} from '@stacks/connect'
const MintNFT = () => {
const appConfig = new AppConfig(['publish_data'])
const userSession = new UserSession({ appConfig})
// setting our network to StackMocknet
const network = new StacksMocknet()
// our mint function that gets called when the button is press
const mint = async () => {
/*
Remember when we were testing our smart contract, we had to mint an NFT first before we do anything else, that is what this component help us do, mint the NFT.
The asset address is the contract deployer's address
*/
const assetAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'
// when we minted the NFT, we minted it to tx-sender, which is the caller of the contract, here we are specifying that the NFT be minted to the user calling the contract
const functionArgs = [
standardPrincipalCV(
userSession.loadUserData().profile.stxAddress.testnet
),
]
// this is a set of parameters clarity needs to construct an openContractCall
const options = {
contractAddress: assetAddress,
contractName: 'sip009',
functionName: 'mint',
functionArgs,
network,
appDetails: {
name: 'sip009',
icon: 'https://assets.website-files.com/618b0aafa4afde65f2fe38fe/618b0aafa4afde2ae1fe3a1f_icon-isotipo.svg',
},
onFinish: (data) => {
// a very simple way to provide feedback to users that the NFT has been minted successfully
window.alert("NFT mint successful, you can whitelist the NFT now, then create an auction");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
},
}
await openContractCall(options)
}
return (
<div>
<button
type='submit' onClick={mint}
className='bg-gray-100
px-6
py-2
rounded-full
border
border-gray-600
hover:border-gray-100
hover:bg-gray-500
hover:text-gray-100
font-semibold'>
Mint NFT
</button>
</div>
);
}
export default MintNFT;
Here too, not so much is going on, you know we defined about four functions in sip009.clar
when we wrote our smart contract, well we are only interested in the mint function, and that is what we hooked up to.
WhitelistNFT.js component
import React, { useEffect, useState } from "react";
import { useConnect } from "@stacks/connect-react";
import { StacksMocknet } from "@stacks/network";
import {
AnchorMode,
PostConditionMode,
NonFungibleConditionCode,
bufferCVFromString,
createAssetInfo,
makeStandardNonFungiblePostCondition,
contractPrincipalCV,
StacksMessageType,
trueCV,
} from "@stacks/transactions";
import { userSession } from "./ConnectWallet";
const WhitelistNFT = () => {
const { doContractCall } = useConnect();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const [assetWhitelist, setAssetWhitelist] = useState("");
const handleAssetWhitelistChange = (e) => {
setAssetWhitelist(e.target.value);
};
const network = new StacksMocknet();
const setWhitelistNFT = async (e) => {
e.preventDefault();
const address = assetWhitelist
// post condition values
const postConditionAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'
const assetAddress = address
const postConditionCode = NonFungibleConditionCode.DoesNotSend;
const assetContractName = "sip009"
const assetName = 'auctionnfts'
const tokenAssetName = bufferCVFromString('auctionnfts')
const type = StacksMessageType.AssetInfo
const nonFungibleAssetInfo = createAssetInfo (
assetAddress,
assetContractName,
assetName,
type
)
/*
Remember when we made contract-calls from clarinet console,
we did it in the format
(contract-call? contract-we-are-calling-from function-we-are-call function-arguments)
*/
const functionArgs = [
/*
Note that we need to construct a contractPrincipal CLarity Type
because the principal we parse is the NFT contract
*/
contractPrincipalCV(
address,
assetContractName
),
trueCV(),
];
// postconditions
const postConditions = [
makeStandardNonFungiblePostCondition(
postConditionAddress,
postConditionCode,
nonFungibleAssetInfo,
tokenAssetName
),
];
// object used as argument to parse into a contract call
const options = {
network,
anchorMode: AnchorMode.Any,
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "set-whitelisted",
functionArgs,
PostConditionMode: PostConditionMode.Deny,
postConditions,
appDetails: {
name: "Auction",
icon: window.location.origin + "/vercel.svg",
},
onFinish: (data) => {
window.alert("Contract Whitelisted, now you can create an auction after a block confirmation");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
},
}
await doContractCall(options);
}
if (!mounted || !userSession.isUserSignedIn()) {
return null;
}
return (
<div>
<form onSubmit={setWhitelistNFT} className='lg:w-1/3 sm:w-2/3 '>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Asset id
</div>
<input
type="text"
value={assetWhitelist}
id='whiteListAssetId'
onChange={handleAssetWhitelistChange}
placeholder="eg ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className='px-5 py-4 '>
<button
type='submit'
className='bg-gray-100 px-6 py-2 rounded-full border border-gray-600 hover:border-gray-100 hover:bg-gray-500 hover:text-gray-100 font-semibold'>
Whitelist NFT
</button>
</div>
</form>
</div>
);
};
export default WhitelistNFT;
All we are doing here is making a contract call to the smart contract with the required arguments converted to clarity types.
CreateAuction.js component
import React, { useEffect, useState } from "react";
import { useConnect, } from "@stacks/connect-react";
import {
AnchorMode,
PostConditionMode,
uintCV,
NonFungibleConditionCode,
bufferCVFromString,
createAssetInfo,
makeStandardNonFungiblePostCondition,
contractPrincipalCV,
StacksMessageType,
tupleCV
} from "@stacks/transactions";
import { userSession } from "./ConnectWallet";
import { StacksMocknet } from "@stacks/network";
const CreateAuction = () => {
/**
* NOTE: this is an NFT transfer event,
* we are sendeing the NFT fro the maker to the contract
* sender: standardPrincipal
* reciever: contractPrincipal
* postConditions: standardNonFunginbleTransfer for contractPrincipal
*/
const { doContractCall } = useConnect();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const [assetId, setAssetId] = useState("")
const [startPrice, setStartPrice] = useState(0)
const [tokenId, setTokenId] = useState(0)
const [auctionDuration, setAuctionDuration] = useState(0)
const network = new StacksMocknet();
const handleAssetIdChange = (e) => {
setAssetId(e.target.value);
};
const handlePriceChange = (e) => {
setStartPrice(e.target.value);
};
const handleTokenIdChange = (e) => {
setTokenId(e.target.value);
};
const handleAuctionDurationChange = (e) => {
setAuctionDuration(e.target.value);
};
const createAuction= async (event) => {
event.preventDefault();
// constructing values for postconditions
const address = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
const assetAddress = address
const postConditionAddress =
userSession.loadUserData().profile.stxAddress.testnet
const nftPostConditionCode = NonFungibleConditionCode.Sends;
const assetContractName = 'sip009'
const assetName = 'auctionnfts'
const tokenAssetName = bufferCVFromString('auctionnfts')
const type = StacksMessageType.AssetInfo
const nonFungibleAssetInfo = createAssetInfo(
assetAddress,
assetContractName,
assetName,
type
)
const functionArgs = [
contractPrincipalCV(
address,
assetContractName
),
tupleCV({
"token-id": uintCV(tokenId),
"start-price": uintCV(startPrice * 1000000),
"expiry": uintCV(auctionDuration),})
];
/*
Post conditions are clarity built-in checker that
help mitigate the potential risk of having a reentrancy
call by ensuring that certain conditions are met otherwise,
the fuction call reverts and the caller lose just
the transaction fees alone
*/
const postConditions = [
makeStandardNonFungiblePostCondition(
postConditionAddress,
nftPostConditionCode,
nonFungibleAssetInfo,
tokenAssetName
),
]
const options = {
network,
anchorMode: AnchorMode.Any,
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "create-auction",
functionArgs,
PostConditionMode: PostConditionMode.Deny,
postConditions,
appDetails: {
name: "Auction",
icon: window.location.origin + "/vercel.svg",
},
onFininsh: (data) => {
window.alert("Auction create successfully");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
}
}
await doContractCall(options);
};
if (!mounted || !userSession.isUserSignedIn()) {
return null;
}
return (
<div>
<form onSubmit={createAuction} className='lg:w-1/3 sm:w-2/3 '>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Asset id
</div>
<input type="text" value={assetId} id='assetId' onChange={handleAssetIdChange} placeholder="eg ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Start Price
</div>
<input
type="number"
value={startPrice}
id='startPrice'
onChange={handlePriceChange}
placeholder='Enter auction start price eg 5000 STX'
className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Token ID
</div>
<input
value={tokenId}
id='tokenId'
type='number'
onChange={handleTokenIdChange}
placeholder='Enter the token ID'
className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Set Duration
</div>
<input
value={auctionDuration}
id='auctionDuration'
type='number'
onChange={handleAuctionDurationChange}
placeholder='Enter the block-heigh at which the auction ends'
className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className='px-5 py-4 '>
<button
type='submit'
className='bg-gray-100 px-6 py-2 rounded-full border border-gray-600 hover:border-gray-100 hover:bg-gray-500 hover:text-gray-100 font-semibold'>
Start Auction
</button>
</div>
</form>
</div>
);
};
export default CreateAuction;
This component basically just exports a button that when clicked calls the create-auction
function from our smart contract and transfers the NFT from the caller to the contract.
PlaceBid.js component
import { AppConfig, useConnect, UserSession } from "@stacks/connect-react";
import {
FungibleConditionCode,
makeStandardSTXPostCondition,
uintCV,
AnchorMode,
PostConditionMode,
contractPrincipalCV,
tupleCV,
} from "@stacks/transactions";
import {useEffect, useState } from "react";
import { StacksMocknet } from "@stacks/network";
const PlaceBid = () => {
/**
* NOTE: this is an STX transfer event,
* we are sendeing the STX from the bider to the contract
* sender: standardPrincipal
* reciever: contractPrincipal
* postConditions: standardPrincipalSTXtransfer for contractPrincipal
*/
const appConfig = new AppConfig(['publish_data'])
const userSession = new UserSession({ appConfig})
const {doContractCall} = useConnect();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), [])
const [assetId, setAssetId] = useState("")
const [bidAmount, setBidAmount] = useState(0);
const [tokenId, setTokeenId] = useState(0);
const [auctionId, setAuctionId] = useState(0);
const handleBidAmountChange = (e) => {
setBidAmount(e.target.value)
}
const network = new StacksMocknet();
const handleTokenIdChange = (e) => {
setTokeenId(e.target.value)
}
const handleAuctionIdChange = (e) =>{
setAuctionId(e.target.value)
}
const handleAssetIdChange = (e) =>{
setAssetId(e.target.value)
}
const handlePlaceBid = async (e) => {
e.preventDefault();
const address = assetId
// postcondition variables
const postConditionAddress =
userSession.loadUserData().profile.stxAddress.testnet
const stxConditionCode = FungibleConditionCode.LessEqual;
const assetContractName = 'sip009'
/*
usually in clarity, STX amounts are in microStacks by default
so we convert them to STX by multiplying the amount by 1000000.
Our biding is in STX not microStacks
*/
const stxConditionAmount = bidAmount * 1000000
const functionArgs = [
contractPrincipalCV(
address,
assetContractName
),
tupleCV({
"token-id": uintCV(tokenId),
"bid-amount": uintCV(bidAmount * 1000000),
"auction-id": uintCV(auctionId),})
]
// postcondition
const postConditions = [
makeStandardSTXPostCondition(
postConditionAddress,
stxConditionCode,
stxConditionAmount
)]
const options = {
network,
anchorMode: AnchorMode.Any,
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "place-a-bid",
functionArgs,
PostConditionMode: PostConditionMode.Deny,
postConditions,
appDetails: {
name: "Auction",
icon: window.location.origin + "/vercel.svg",
},
onFininsh: (data) => {
window.alert("Bid placed successfully");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
}
}
await doContractCall(options);
}
if(!mounted || !userSession.isUserSignedIn()){
return null
}
return (
<div>
<form onSubmit={handlePlaceBid} className='lg:w-1/3 sm:w-2/3 '>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Asset id
</div>
<input type="text" value={assetId} id='assetId' onChange={handleAssetIdChange} placeholder="eg ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Bid Amount
</div>
<input onChange={handleBidAmountChange} type='number' value={bidAmount} placeholder='Bids are in STX eg 4 means 4 STX' className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Token-id
</div>
<input onChange={handleTokenIdChange} type='number' value={tokenId} placeholder='Enter token id of your bid interest' className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Auction-id
</div>
<input onChange={handleAuctionIdChange} type='number' value={auctionId} placeholder='Enter the auction id of the bid' className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className='px-5 py-4 justify-evenly'>
<button type='submit' className='bg-gray-100 px-6 py-2 rounded-full border border-gray-600 hover:border-gray-100 hover:bg-gray-500 hover:text-gray-100 '>
Place Bid
</button>
</div>
</form>
</div>
);
}
export default PlaceBid;
So when a user places a bid, they transfer their bid amount to the contract as well, so essentially, they make an STX transfer to the contract
Finally, we said if the user won the bid, they can claim their win by providing some details, likewise, if they did not win, they can request a refund on their bid amount.
SettleAuction.js & RequestRefund.js component
import React, { useCallback,useEffect, useState } from "react";
import { useConnect } from "@stacks/connect-react";
import { StacksMocknet } from "@stacks/network";
import {
AnchorMode,
PostConditionMode,
NonFungibleConditionCode,
FungibleConditionCode,
bufferCVFromString,
createAssetInfo,
makeStandardNonFungiblePostCondition,
makeStandardSTXPostCondition,
standardPrincipalCV,
contractPrincipalCV,
StacksMessageType,
uintCV,
callReadOnlyFunction
} from "@stacks/transactions";
import { userSession } from "./ConnectWallet";
// run npm i @use-it/interval
import useInterval from "@use-it/interval";
const SettleAuction = () => {
const { doContractCall } = useConnect();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const [assetId, setAssetId] = useState("");
const [auctionId, setAuctionId] = useState(0);
const [bidersBid, setBidersBid] = useState(0);
const [highestBidAmount, setHighestBidAmount] = useState(0);
const handleAuctionIdChange = (e) =>{
setAuctionId(e.target.value)
}
const handleAssetIdChange = (e) => {
setAssetId(e.target.value);
};
const network = new StacksMocknet();
// function to request refund
const handleRequestRefund = async (e) =>{
e.preventDefault();
const address = assetId
// post condition values
const postConditionAddress =
userSession.loadUserData().profile.stxAddress.testnet
const stxConditionCode = FungibleConditionCode.LessEqual
const assetContractName = 'sip009'
const stxConditionAmount = bidersBid
const functionArgs = [
contractPrincipalCV(
address,
assetContractName
),
uintCV(auctionId),
];
// postconditions
const postConditions = [
makeStandardSTXPostCondition(
postConditionAddress,
stxConditionCode,
stxConditionAmount
)]
const options = {
network,
anchorMode: AnchorMode.Any,
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "request-refund",
functionArgs,
PostConditionMode: PostConditionMode.Deny,
postConditions,
appDetails: {
name: "Auction",
icon: window.location.origin + "/vercel.svg",
},
onFinish: (data) => {
window.alert("Refund request successful, wait for block confirmation to get your refund");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
},
}
await doContractCall(options);
}
// fetch bidder's bid
const getBidersBid = useCallback(async () => {
if (userSession.isUserSignedIn()) {
const userAddress = userSession.loadUserData().profile.stxAddress.testnet
const callOptions = {
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "get-biders-total-bid",
network: new StacksMocknet(),
functionArgs: [standardPrincipalCV(
userAddress
)],
};
const result = await callReadOnlyFunction(callOptions);
console.log(result);
if (result.value) {
setBidersBid(result.value)
}
}
});
// fetch Bider's Bid every second
useInterval(getBidersBid, 10000);
// function to claim win
const handleClaimWin = async (e) => {
e.preventDefault();
const address = assetId
// post condition values
const postConditionAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'
const assetAddress = address
const postConditionCode = NonFungibleConditionCode.Sends;
const assetContractName = "sip009"
const assetName = 'auctionnfts'
const tokenAssetName = bufferCVFromString('auctionnfts')
const type = StacksMessageType.AssetInfo
const nonFungibleAssetInfo = createAssetInfo (
assetAddress,
assetContractName,
assetName,
type
)
const stxConditionCode = FungibleConditionCode.LessEqual;
const stxConditionAmount = highestBidAmount;
const functionArgs = [
contractPrincipalCV(
address,
assetContractName
),
uintCV(auctionId),
];
// postconditions
const postConditions = [
makeStandardNonFungiblePostCondition(
postConditionAddress,
postConditionCode,
nonFungibleAssetInfo,
tokenAssetName
),
makeStandardSTXPostCondition(
postConditionAddress,
stxConditionCode,
stxConditionAmount
)
];
const options = {
network,
anchorMode: AnchorMode.Any,
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "settle-auction",
functionArgs,
PostConditionMode: PostConditionMode.Deny,
postConditions,
appDetails: {
name: "Auction",
icon: window.location.origin + "/vercel.svg",
},
onFinish: (data) => {
window.alert("Win claim successful, wait for block confirmation to get your claim");
console.log("Stacks Transaction:", data.stacksTransaction);
console.log("Transaction ID:", data.txId);
console.log("Raw transaction:", data.txRaw);
},
}
await doContractCall(options);
}
// fetch bider's bid
const getHeighestBidAmount = useCallback(async () => {
if (userSession.isUserSignedIn()) {
const userAddress = userSession.loadUserData().profile.stxAddress.testnet
const callOptions = {
contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
contractName: "auction",
functionName: "get-highest-bid-amount",
network: new StacksMocknet(),
functionArgs: [standardPrincipalCV(
userAddress
)],
};
const result = await callReadOnlyFunction(callOptions);
console.log(result);
if (result.value) {
setBidersBid(result.value)
}
}
});
// fetch Heighest Bid Amount every second
useInterval(getHeighestBidAmount, 10000);
if (!mounted || !userSession.isUserSignedIn()) {
return null;
}
return (
<div>
<form className='lg:w-1/3 sm:w-2/3 '>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Asset id
</div>
<input
type="text"
value={assetId}
id='whiteListAssetId'
onChange={handleAssetIdChange}
placeholder="eg ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"
className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
<div className=' flex items-center border border-gray-600 my-4 mx-4 rounded'>
<div className='flex-shrink-0 bg-gray-600 text-gray-100 text-sm py-2 px-6'>
Auction-id
</div>
<input onChange={handleAuctionIdChange} type='number' value={auctionId} placeholder='Enter the auction id of the bid' className='appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none' />
</div>
</form>
<div className='px-5 py-4 '>
<button
onClick={handleClaimWin}
type='button'
className='
bg-gray-100
px-6
py-2
rounded-full
border
border-gray-600
hover:border-gray-100
hover:bg-gray-500
hover:text-gray-100
font-semibold
mr-8'>
Claim Win
</button>
<button
onClick={handleRequestRefund}
type='button'
className='
bg-gray-100
px-6
py-2
rounded-full
border
border-gray-600
hover:border-gray-100
hover:bg-gray-500
hover:text-gray-100
font-semibold
ml-8'>
Request Refund
</button>
</div>
</div>
);
};
export default SettleAuction;
This is quite a lengthy one but what is going in is simple. We are exporting a component called SettleAuction, which has a couple of functions. The first function is the handleRequestRefund
, it takes the asset identifier and auction id as inputs and requests a refund using the caller's address to check for the caller's bid amount. the handleClaimWin
function takes the same functions and transfers the NFT from the contract to the bid winner and the highest bid amount to the maker.
Congratulations, you have created a full stack dapp for an NFT Auction place, this by no means is an implementation that would be suitable for an enterprise solution. It is intended to serve as a mere example for illustrating how to use Clarity to create a smart contract and stack.js to hook the smart contract to a frontend. If you found value, you can follow me on social @mosnyik. Thank you for your time.
Subscribe to my newsletter
Read articles from Nyikwagh Moses directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by