Source Code

;; NFT Marketplace Contract - Simplified Version
;; Enables listing, buying, selling, and offer management with platform fees
;; This version uses approval-based transfers instead of escrow

;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u200))
(define-constant err-not-token-owner (err u201))
(define-constant err-listing-not-found (err u202))
(define-constant err-listing-expired (err u203))
(define-constant err-insufficient-payment (err u204))
(define-constant err-nft-not-listed (err u205))
(define-constant err-unauthorized (err u206))
(define-constant err-invalid-price (err u207))
(define-constant err-offer-not-found (err u208))
(define-constant err-offer-expired (err u209))
(define-constant err-invalid-offer (err u210))
(define-constant err-listing-active (err u211))
(define-constant err-transfer-failed (err u212))

;; Platform fee is 2.5% (250 basis points out of 10000)
(define-constant platform-fee-basis-points u250)
(define-constant basis-points-divisor u10000)

;; Data Variables
(define-data-var platform-fee-recipient principal contract-owner)
(define-data-var marketplace-enabled bool true)
(define-data-var next-listing-id uint u1)
(define-data-var next-offer-id uint u1)

;; Listing status constants
(define-constant listing-status-active u1)
(define-constant listing-status-sold u2)
(define-constant listing-status-cancelled u3)

;; Offer status constants
(define-constant offer-status-pending u1)
(define-constant offer-status-accepted u2)
(define-constant offer-status-rejected u3)
(define-constant offer-status-cancelled u4)

;; Data Maps
(define-map listings 
    uint 
    {
        token-id: uint,
        nft-contract: principal,
        seller: principal,
        price: uint,
        status: uint,
        expiry: uint,
        created-at: uint
    }
)

(define-map token-listings 
    {nft-contract: principal, token-id: uint} 
    uint
)

(define-map offers 
    uint 
    {
        token-id: uint,
        nft-contract: principal,
        offerer: principal,
        amount: uint,
        status: uint,
        expiry: uint,
        created-at: uint
    }
)

(define-map token-offers 
    {nft-contract: principal, token-id: uint} 
    (list 50 uint)
)

;; Read-only functions

(define-read-only (get-listing (listing-id uint))
    (ok (map-get? listings listing-id))
)

(define-read-only (get-listing-by-token (nft-contract principal) (token-id uint))
    (match (map-get? token-listings {nft-contract: nft-contract, token-id: token-id})
        listing-id (ok (map-get? listings listing-id))
        (ok none)
    )
)

(define-read-only (get-offer (offer-id uint))
    (ok (map-get? offers offer-id))
)

(define-read-only (get-token-offers (nft-contract principal) (token-id uint))
    (ok (default-to (list) (map-get? token-offers {nft-contract: nft-contract, token-id: token-id})))
)

(define-read-only (calculate-platform-fee (price uint))
    (ok (/ (* price platform-fee-basis-points) basis-points-divisor))
)

(define-read-only (get-platform-fee-recipient)
    (ok (var-get platform-fee-recipient))
)

(define-read-only (is-marketplace-enabled)
    (ok (var-get marketplace-enabled))
)

(define-read-only (get-next-listing-id)
    (ok (var-get next-listing-id))
)

(define-read-only (get-next-offer-id)
    (ok (var-get next-offer-id))
)

;; Private functions

(define-private (transfer-stx (amount uint) (sender principal) (recipient principal))
    (if (> amount u0)
        (stx-transfer? amount sender recipient)
        (ok true)
    )
)

;; Public functions

(define-public (list-nft 
    (nft-contract principal)
    (token-id uint) 
    (price uint) 
    (expiry uint))
    (let 
        (
            (listing-id (var-get next-listing-id))
        )
        ;; Validations
        (asserts! (var-get marketplace-enabled) err-unauthorized)
        (asserts! (> price u0) err-invalid-price)
        (asserts! (> expiry stacks-block-height) err-listing-expired)
        
        ;; Check if NFT is already listed
        (asserts! (is-none (map-get? token-listings {nft-contract: nft-contract, token-id: token-id})) 
            err-listing-active)
        
        ;; Create listing
        (map-set listings listing-id {
            token-id: token-id,
            nft-contract: nft-contract,
            seller: tx-sender,
            price: price,
            status: listing-status-active,
            expiry: expiry,
            created-at: stacks-block-height
        })
        
        (map-set token-listings {nft-contract: nft-contract, token-id: token-id} listing-id)
        
        ;; Increment listing counter
        (var-set next-listing-id (+ listing-id u1))
        
        (print {
            event: "list",
            listing-id: listing-id,
            token-id: token-id,
            nft-contract: nft-contract,
            seller: tx-sender,
            price: price,
            expiry: expiry
        })
        
        (ok listing-id)
    )
)

(define-public (unlist-nft (listing-id uint))
    (let 
        (
            (listing (unwrap! (map-get? listings listing-id) err-listing-not-found))
            (nft-contract (get nft-contract listing))
            (token-id (get token-id listing))
            (seller (get seller listing))
        )
        ;; Validations
        (asserts! (is-eq tx-sender seller) err-not-token-owner)
        (asserts! (is-eq (get status listing) listing-status-active) err-nft-not-listed)
        
        ;; Update listing status
        (map-set listings listing-id (merge listing {status: listing-status-cancelled}))
        (map-delete token-listings {nft-contract: nft-contract, token-id: token-id})
        
        (print {
            event: "unlist",
            listing-id: listing-id,
            token-id: token-id,
            seller: seller
        })
        
        (ok true)
    )
)

(define-public (buy-nft 
    (listing-id uint)
    (nft-contract principal))
    (let 
        (
            (listing (unwrap! (map-get? listings listing-id) err-listing-not-found))
            (token-id (get token-id listing))
            (seller (get seller listing))
            (price (get price listing))
            (platform-fee (unwrap! (calculate-platform-fee price) err-invalid-price))
            (seller-proceeds (- price platform-fee))
        )
        ;; Validations
        (asserts! (var-get marketplace-enabled) err-unauthorized)
        (asserts! (is-eq (get status listing) listing-status-active) err-nft-not-listed)
        (asserts! (<= stacks-block-height (get expiry listing)) err-listing-expired)
        (asserts! (is-eq nft-contract (get nft-contract listing)) err-unauthorized)
        
        ;; Transfer payment to seller
        (try! (transfer-stx seller-proceeds tx-sender seller))
        
        ;; Transfer platform fee
        (try! (transfer-stx platform-fee tx-sender (var-get platform-fee-recipient)))
        
        ;; Update listing status
        (map-set listings listing-id (merge listing {status: listing-status-sold}))
        (map-delete token-listings {nft-contract: nft-contract, token-id: token-id})
        
        (print {
            event: "purchase",
            listing-id: listing-id,
            token-id: token-id,
            buyer: tx-sender,
            seller: seller,
            price: price,
            platform-fee: platform-fee,
            note: "Buyer must call NFT contract transfer function separately"
        })
        
        (ok true)
    )
)

(define-public (make-offer 
    (nft-contract principal) 
    (token-id uint) 
    (amount uint) 
    (expiry uint))
    (let 
        (
            (offer-id (var-get next-offer-id))
            (existing-offers (default-to (list) (map-get? token-offers {nft-contract: nft-contract, token-id: token-id})))
        )
        ;; Validations
        (asserts! (var-get marketplace-enabled) err-unauthorized)
        (asserts! (> amount u0) err-invalid-offer)
        (asserts! (> expiry stacks-block-height) err-offer-expired)
        
        ;; Create offer
        (map-set offers offer-id {
            token-id: token-id,
            nft-contract: nft-contract,
            offerer: tx-sender,
            amount: amount,
            status: offer-status-pending,
            expiry: expiry,
            created-at: stacks-block-height
        })
        
        ;; Add to token offers list (if space available)
        (if (< (len existing-offers) u50)
            (map-set token-offers 
                {nft-contract: nft-contract, token-id: token-id}
                (unwrap-panic (as-max-len? (append existing-offers offer-id) u50))
            )
            true
        )
        
        ;; Increment offer counter
        (var-set next-offer-id (+ offer-id u1))
        
        (print {
            event: "offer-made",
            offer-id: offer-id,
            token-id: token-id,
            nft-contract: nft-contract,
            offerer: tx-sender,
            amount: amount,
            expiry: expiry
        })
        
        (ok offer-id)
    )
)

(define-public (accept-offer 
    (offer-id uint)
    (nft-contract principal))
    (let 
        (
            (offer (unwrap! (map-get? offers offer-id) err-offer-not-found))
            (token-id (get token-id offer))
            (offerer (get offerer offer))
            (amount (get amount offer))
            (platform-fee (unwrap! (calculate-platform-fee amount) err-invalid-price))
            (seller-proceeds (- amount platform-fee))
        )
        ;; Validations
        (asserts! (var-get marketplace-enabled) err-unauthorized)
        (asserts! (is-eq (get status offer) offer-status-pending) err-invalid-offer)
        (asserts! (<= stacks-block-height (get expiry offer)) err-offer-expired)
        (asserts! (is-eq nft-contract (get nft-contract offer)) err-unauthorized)
        
        ;; If there's an active listing, cancel it
        (match (map-get? token-listings {nft-contract: nft-contract, token-id: token-id})
            listing-id (begin
                (map-set listings listing-id 
                    (merge (unwrap-panic (map-get? listings listing-id)) {status: listing-status-cancelled}))
                (map-delete token-listings {nft-contract: nft-contract, token-id: token-id})
            )
            true
        )
        
        ;; Update offer status
        (map-set offers offer-id (merge offer {status: offer-status-accepted}))
        
        (print {
            event: "offer-accepted",
            offer-id: offer-id,
            token-id: token-id,
            seller: tx-sender,
            buyer: offerer,
            amount: amount,
            platform-fee: platform-fee,
            note: "Parties must exchange STX and NFT separately via payment and transfer calls"
        })
        
        (ok true)
    )
)

(define-public (cancel-offer (offer-id uint))
    (let 
        (
            (offer (unwrap! (map-get? offers offer-id) err-offer-not-found))
            (offerer (get offerer offer))
        )
        ;; Validations
        (asserts! (is-eq tx-sender offerer) err-unauthorized)
        (asserts! (is-eq (get status offer) offer-status-pending) err-invalid-offer)
        
        ;; Update offer status
        (map-set offers offer-id (merge offer {status: offer-status-cancelled}))
        
        (print {
            event: "offer-cancelled",
            offer-id: offer-id,
            offerer: offerer
        })
        
        (ok true)
    )
)

(define-public (reject-offer 
    (offer-id uint)
    (nft-contract principal))
    (let 
        (
            (offer (unwrap! (map-get? offers offer-id) err-offer-not-found))
            (token-id (get token-id offer))
            (offerer (get offerer offer))
        )
        ;; Validations
        (asserts! (is-eq (get status offer) offer-status-pending) err-invalid-offer)
        (asserts! (is-eq nft-contract (get nft-contract offer)) err-unauthorized)
        
        ;; Update offer status
        (map-set offers offer-id (merge offer {status: offer-status-rejected}))
        
        (print {
            event: "offer-rejected",
            offer-id: offer-id,
            token-id: token-id,
            owner: tx-sender
        })
        
        (ok true)
    )
)

;; Admin functions

(define-public (set-platform-fee-recipient (new-recipient principal))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set platform-fee-recipient new-recipient)
        (ok true)
    )
)

(define-public (toggle-marketplace)
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set marketplace-enabled (not (var-get marketplace-enabled)))
        (ok (var-get marketplace-enabled))
    )
)

Functions (19)

FunctionAccessArgs
get-listingread-onlylisting-id: uint
get-listing-by-tokenread-onlynft-contract: principal, token-id: uint
get-offerread-onlyoffer-id: uint
get-token-offersread-onlynft-contract: principal, token-id: uint
calculate-platform-feeread-onlyprice: uint
get-platform-fee-recipientread-only
is-marketplace-enabledread-only
get-next-listing-idread-only
get-next-offer-idread-only
transfer-stxprivateamount: uint, sender: principal, recipient: principal
list-nftpublicnft-contract: principal, token-id: uint, price: uint, expiry: uint
unlist-nftpubliclisting-id: uint
buy-nftpubliclisting-id: uint, nft-contract: principal
make-offerpublicnft-contract: principal, token-id: uint, amount: uint, expiry: uint
accept-offerpublicoffer-id: uint, nft-contract: principal
cancel-offerpublicoffer-id: uint
reject-offerpublicoffer-id: uint, nft-contract: principal
set-platform-fee-recipientpublicnew-recipient: principal
toggle-marketplacepublic