stacksmint-marketplace-v2-1-3

SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N

Source Code

;; StacksMint Marketplace Contract v2.1
;; Decentralized NFT trading platform with offers, auctions, bundles, and analytics
;; Version: 2.1.0
;; Author: StacksMint Team
;; Upgrade: v2.1 - Bundle sales, auction system, price history, escrow

;; ============================================================================
;; Error Constants
;; ============================================================================

(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_OWNER (err u100))
(define-constant ERR_ALREADY_LISTED (err u101))
(define-constant ERR_NOT_LISTED (err u102))
(define-constant ERR_INVALID_PRICE (err u103))
(define-constant ERR_SELLER_ONLY (err u104))
(define-constant ERR_CANNOT_BUY_OWN (err u105))
(define-constant ERR_OFFER_NOT_FOUND (err u106))
(define-constant ERR_OFFER_EXPIRED (err u107))
(define-constant ERR_INSUFFICIENT_OFFER (err u108))
(define-constant ERR_LISTING_EXPIRED (err u109))
(define-constant ERR_MARKETPLACE_PAUSED (err u110))
(define-constant ERR_AUCTION_ACTIVE (err u111))
(define-constant ERR_AUCTION_ENDED (err u112))
(define-constant ERR_BID_TOO_LOW (err u113))
(define-constant ERR_BUNDLE_EMPTY (err u114))
(define-constant ERR_BUNDLE_TOO_LARGE (err u115))
(define-constant ERR_ESCROW_LOCKED (err u116))
(define-constant ERR_NOT_BIDDER (err u117))

;; ============================================================================
;; Configuration Constants
;; ============================================================================

(define-constant MIN_LISTING_PRICE u1000)       ;; 0.001 STX minimum
(define-constant DEFAULT_LISTING_DURATION u4320) ;; ~30 days in blocks
(define-constant MAX_LISTING_DURATION u17280)   ;; ~120 days in blocks
(define-constant OFFER_DURATION u1440)          ;; ~10 days in blocks
(define-constant MIN_BID_INCREMENT u50)         ;; 5% minimum bid increment
(define-constant MAX_BUNDLE_SIZE u10)           ;; Maximum NFTs per bundle
(define-constant AUCTION_EXTENSION u72)         ;; ~12 hours extension on last-minute bids

;; ============================================================================
;; Data Variables
;; ============================================================================

(define-data-var marketplace-paused bool false)
(define-data-var total-volume uint u0)
(define-data-var total-sales uint u0)
(define-data-var listing-counter uint u0)
(define-data-var auction-counter uint u0)
(define-data-var bundle-counter uint u0)

;; ============================================================================
;; Data Maps
;; ============================================================================

;; Active listings
(define-map listings uint {
  price: uint,
  seller: principal,
  listed-at: uint,
  expires-at: uint
})

;; Offers on tokens
(define-map offers { token-id: uint, offerer: principal } {
  amount: uint,
  expires-at: uint
})

;; Best offer per token (for quick lookup)
(define-map best-offers uint {
  offerer: principal,
  amount: uint
})

;; Floor price per collection
(define-map collection-floors uint uint)

;; Auctions
(define-map auctions uint {
  token-id: uint,
  seller: principal,
  reserve-price: uint,
  current-bid: uint,
  highest-bidder: (optional principal),
  start-block: uint,
  end-block: uint,
  settled: bool
})

;; Auction bids (for refunds)
(define-map auction-bids { auction-id: uint, bidder: principal } uint)

;; Bundle listings
(define-map bundles uint {
  token-ids: (list 10 uint),
  seller: principal,
  price: uint,
  listed-at: uint,
  expires-at: uint
})

;; Price history (last 10 sales per token)
(define-map price-history uint (list 10 { price: uint, block: uint, buyer: principal }))

;; Escrow balances
(define-map escrow-balances principal uint)

;; ============================================================================
;; Authorization Helpers
;; ============================================================================

(define-private (is-listing-active (token-id uint))
  (match (map-get? listings token-id)
    listing (< block-height (get expires-at listing))
    false))

(define-private (is-offer-valid (token-id uint) (offerer principal))
  (match (map-get? offers { token-id: token-id, offerer: offerer })
    offer (< block-height (get expires-at offer))
    false))

(define-private (is-auction-active (auction-id uint))
  (match (map-get? auctions auction-id)
    auction (and 
      (>= block-height (get start-block auction))
      (< block-height (get end-block auction))
      (not (get settled auction)))
    false))

;; ============================================================================
;; Listing Functions
;; ============================================================================

;; List NFT for sale
(define-public (list-nft (token-id uint) (price uint))
  (list-nft-with-expiry token-id price DEFAULT_LISTING_DURATION))

;; List NFT with custom expiry
(define-public (list-nft-with-expiry (token-id uint) (price uint) (duration uint))
  (begin
    (asserts! (not (var-get marketplace-paused)) ERR_MARKETPLACE_PAUSED)
    (asserts! (>= price MIN_LISTING_PRICE) ERR_INVALID_PRICE)
    (asserts! (<= duration MAX_LISTING_DURATION) ERR_INVALID_PRICE)
    (asserts! (not (is-listing-active token-id)) ERR_ALREADY_LISTED)
    (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
    (var-set listing-counter (+ (var-get listing-counter) u1))
    (map-set listings token-id {
      price: price,
      seller: tx-sender,
      listed-at: block-height,
      expires-at: (+ block-height duration)
    })
    (ok token-id)))

;; Cancel listing
(define-public (cancel-listing (token-id uint))
  (let ((listing (unwrap! (map-get? listings token-id) ERR_NOT_LISTED)))
    (asserts! (is-eq tx-sender (get seller listing)) ERR_SELLER_ONLY)
    (map-delete listings token-id)
    (ok true)))

;; Update listing price
(define-public (update-listing-price (token-id uint) (new-price uint))
  (let ((listing (unwrap! (map-get? listings token-id) ERR_NOT_LISTED)))
    (asserts! (is-eq tx-sender (get seller listing)) ERR_SELLER_ONLY)
    (asserts! (>= new-price MIN_LISTING_PRICE) ERR_INVALID_PRICE)
    (map-set listings token-id (merge listing { price: new-price }))
    (ok new-price)))

;; Buy listed NFT
(define-public (buy-nft (token-id uint))
  (let ((listing (unwrap! (map-get? listings token-id) ERR_NOT_LISTED)))
    (asserts! (is-listing-active token-id) ERR_LISTING_EXPIRED)
    (asserts! (not (is-eq tx-sender (get seller listing))) ERR_CANNOT_BUY_OWN)
    (let ((price (get price listing))
          (seller (get seller listing))
          (metadata (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 get-token-metadata token-id))
          (royalty-percent (match metadata data (get royalty-percent data) u0))
          (creator (match metadata data (get creator data) seller)))
      ;; Collect marketplace fee and royalty
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-marketplace-fee price creator royalty-percent))
      ;; Calculate net payment to seller
      (let ((marketplace-fee (/ (* price u25) u1000))
            (royalty (/ (* price royalty-percent) u1000))
            (net-payment (- price (+ marketplace-fee royalty))))
        ;; Transfer payment to seller
        (try! (stx-transfer? net-payment tx-sender seller))
        ;; Transfer NFT
        (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 transfer token-id seller tx-sender))
        ;; Update stats
        (var-set total-volume (+ (var-get total-volume) price))
        (var-set total-sales (+ (var-get total-sales) u1))
        ;; Record price history
        (record-sale token-id price tx-sender)
        ;; Clean up listing
        (map-delete listings token-id)
        (ok { token-id: token-id, price: price, seller: seller, buyer: tx-sender })))))

;; ============================================================================
;; Offer Functions
;; ============================================================================

;; Make offer on NFT
(define-public (make-offer (token-id uint) (amount uint))
  (begin
    (asserts! (>= amount MIN_LISTING_PRICE) ERR_INVALID_PRICE)
    ;; Lock funds in escrow
    (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
    (map-set escrow-balances tx-sender (+ (default-to u0 (map-get? escrow-balances tx-sender)) amount))
    (map-set offers { token-id: token-id, offerer: tx-sender } {
      amount: amount,
      expires-at: (+ block-height OFFER_DURATION)
    })
    ;; Update best offer if higher
    (match (map-get? best-offers token-id)
      current (if (> amount (get amount current))
        (map-set best-offers token-id { offerer: tx-sender, amount: amount })
        true)
      (map-set best-offers token-id { offerer: tx-sender, amount: amount }))
    (ok amount)))

;; Cancel offer and get refund
(define-public (cancel-offer (token-id uint))
  (let ((offer (unwrap! (map-get? offers { token-id: token-id, offerer: tx-sender }) ERR_OFFER_NOT_FOUND)))
    ;; Refund from escrow
    (try! (as-contract (stx-transfer? (get amount offer) tx-sender tx-sender)))
    (map-set escrow-balances tx-sender 
      (- (default-to u0 (map-get? escrow-balances tx-sender)) (get amount offer)))
    (map-delete offers { token-id: token-id, offerer: tx-sender })
    (ok true)))

;; Accept offer
(define-public (accept-offer (token-id uint) (offerer principal))
  (let ((offer (unwrap! (map-get? offers { token-id: token-id, offerer: offerer }) ERR_OFFER_NOT_FOUND)))
    (asserts! (is-offer-valid token-id offerer) ERR_OFFER_EXPIRED)
    (let ((amount (get amount offer))
          (metadata (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 get-token-metadata token-id))
          (royalty-percent (match metadata data (get royalty-percent data) u0))
          (creator (match metadata data (get creator data) tx-sender)))
      ;; Collect fees from escrow
      (try! (as-contract (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-marketplace-fee amount creator royalty-percent)))
      ;; Calculate net payment
      (let ((marketplace-fee (/ (* amount u25) u1000))
            (royalty (/ (* amount royalty-percent) u1000))
            (net-payment (- amount (+ marketplace-fee royalty))))
        ;; Transfer payment from escrow to seller
        (try! (as-contract (stx-transfer? net-payment tx-sender tx-sender)))
        ;; Transfer NFT
        (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 transfer token-id tx-sender offerer))
        ;; Update stats
        (var-set total-volume (+ (var-get total-volume) amount))
        (var-set total-sales (+ (var-get total-sales) u1))
        ;; Record price history
        (record-sale token-id amount offerer)
        ;; Clean up
        (map-delete offers { token-id: token-id, offerer: offerer })
        (map-set escrow-balances offerer 
          (- (default-to u0 (map-get? escrow-balances offerer)) amount))
        (ok { token-id: token-id, price: amount, seller: tx-sender, buyer: offerer })))))

;; ============================================================================
;; Auction Functions
;; ============================================================================

;; Create auction
(define-public (create-auction (token-id uint) (reserve-price uint) (duration uint))
  (begin
    (asserts! (not (var-get marketplace-paused)) ERR_MARKETPLACE_PAUSED)
    (asserts! (>= reserve-price MIN_LISTING_PRICE) ERR_INVALID_PRICE)
    (asserts! (<= duration MAX_LISTING_DURATION) ERR_INVALID_PRICE)
    (asserts! (not (is-listing-active token-id)) ERR_ALREADY_LISTED)
    (let ((auction-id (+ (var-get auction-counter) u1)))
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      (map-set auctions auction-id {
        token-id: token-id,
        seller: tx-sender,
        reserve-price: reserve-price,
        current-bid: u0,
        highest-bidder: none,
        start-block: block-height,
        end-block: (+ block-height duration),
        settled: false
      })
      (var-set auction-counter auction-id)
      (ok auction-id))))

;; Place bid on auction
(define-public (place-bid (auction-id uint) (bid-amount uint))
  (let ((auction (unwrap! (map-get? auctions auction-id) ERR_NOT_LISTED)))
    (asserts! (is-auction-active auction-id) ERR_AUCTION_ENDED)
    (asserts! (not (is-eq tx-sender (get seller auction))) ERR_CANNOT_BUY_OWN)
    (let ((current-bid (get current-bid auction))
          (min-bid (if (is-eq current-bid u0)
            (get reserve-price auction)
            (+ current-bid (/ (* current-bid MIN_BID_INCREMENT) u1000)))))
      (asserts! (>= bid-amount min-bid) ERR_BID_TOO_LOW)
      ;; Refund previous bidder
      (match (get highest-bidder auction)
        prev-bidder (try! (as-contract (stx-transfer? current-bid tx-sender prev-bidder)))
        true)
      ;; Accept new bid (escrow)
      (try! (stx-transfer? bid-amount tx-sender (as-contract tx-sender)))
      ;; Extend auction if bid in last 12 hours
      (let ((new-end (if (< (- (get end-block auction) block-height) AUCTION_EXTENSION)
            (+ (get end-block auction) AUCTION_EXTENSION)
            (get end-block auction))))
        (map-set auctions auction-id (merge auction {
          current-bid: bid-amount,
          highest-bidder: (some tx-sender),
          end-block: new-end
        }))
        (map-set auction-bids { auction-id: auction-id, bidder: tx-sender } bid-amount)
        (ok bid-amount)))))

;; Settle auction
(define-public (settle-auction (auction-id uint))
  (let ((auction (unwrap! (map-get? auctions auction-id) ERR_NOT_LISTED)))
    (asserts! (>= block-height (get end-block auction)) ERR_AUCTION_ACTIVE)
    (asserts! (not (get settled auction)) ERR_AUCTION_ENDED)
    (match (get highest-bidder auction)
      winner (let (
          (token-id (get token-id auction))
          (final-price (get current-bid auction))
          (seller (get seller auction))
          (metadata (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 get-token-metadata token-id))
          (royalty-percent (match metadata data (get royalty-percent data) u0))
          (creator (match metadata data (get creator data) seller)))
        ;; Collect fees
        (try! (as-contract (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-marketplace-fee final-price creator royalty-percent)))
        ;; Calculate net payment
        (let ((marketplace-fee (/ (* final-price u25) u1000))
              (royalty (/ (* final-price royalty-percent) u1000))
              (net-payment (- final-price (+ marketplace-fee royalty))))
          ;; Transfer payment from escrow to seller
          (try! (as-contract (stx-transfer? net-payment tx-sender seller)))
          ;; Transfer NFT
          (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 transfer token-id seller winner))
          ;; Update stats
          (var-set total-volume (+ (var-get total-volume) final-price))
          (var-set total-sales (+ (var-get total-sales) u1))
          ;; Record price history
          (record-sale token-id final-price winner)
          ;; Mark settled
          (map-set auctions auction-id (merge auction { settled: true }))
          (ok { auction-id: auction-id, winner: winner, price: final-price })))
      ;; No bids - just mark settled
      (begin
        (map-set auctions auction-id (merge auction { settled: true }))
        (ok { auction-id: auction-id, winner: (get seller auction), price: u0 })))))

;; ============================================================================
;; Bundle Functions
;; ============================================================================

;; Create bundle listing
(define-public (create-bundle (token-ids (list 10 uint)) (price uint))
  (begin
    (asserts! (not (var-get marketplace-paused)) ERR_MARKETPLACE_PAUSED)
    (asserts! (> (len token-ids) u0) ERR_BUNDLE_EMPTY)
    (asserts! (<= (len token-ids) MAX_BUNDLE_SIZE) ERR_BUNDLE_TOO_LARGE)
    (asserts! (>= price MIN_LISTING_PRICE) ERR_INVALID_PRICE)
    (let ((bundle-id (+ (var-get bundle-counter) u1)))
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      (map-set bundles bundle-id {
        token-ids: token-ids,
        seller: tx-sender,
        price: price,
        listed-at: block-height,
        expires-at: (+ block-height DEFAULT_LISTING_DURATION)
      })
      (var-set bundle-counter bundle-id)
      (ok bundle-id))))

;; Buy bundle
(define-public (buy-bundle (bundle-id uint))
  (let ((bundle (unwrap! (map-get? bundles bundle-id) ERR_NOT_LISTED)))
    (asserts! (< block-height (get expires-at bundle)) ERR_LISTING_EXPIRED)
    (asserts! (not (is-eq tx-sender (get seller bundle))) ERR_CANNOT_BUY_OWN)
    (let ((price (get price bundle))
          (seller (get seller bundle)))
      ;; Collect marketplace fee (no individual royalties for bundles)
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-marketplace-fee price seller u0))
      (let ((marketplace-fee (/ (* price u25) u1000))
            (net-payment (- price marketplace-fee)))
        ;; Transfer payment
        (try! (stx-transfer? net-payment tx-sender seller))
        ;; Transfer all NFTs in bundle
        (try! (fold transfer-bundle-nft (get token-ids bundle) (ok seller)))
        ;; Update stats
        (var-set total-volume (+ (var-get total-volume) price))
        (var-set total-sales (+ (var-get total-sales) u1))
        ;; Clean up
        (map-delete bundles bundle-id)
        (ok { bundle-id: bundle-id, price: price })))))

(define-private (transfer-bundle-nft (token-id uint) (prev (response principal uint)))
  (match prev
    seller (match (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-nft-v2-1-3 transfer token-id seller tx-sender)
      result (ok seller)
      err prev)
    err prev))

;; ============================================================================
;; Price History Functions
;; ============================================================================

(define-private (record-sale (token-id uint) (price uint) (buyer principal))
  (let ((current-history (default-to (list) (map-get? price-history token-id)))
        (new-entry { price: price, block: block-height, buyer: buyer }))
    (match (as-max-len? (concat (list new-entry) current-history) u10)
      new-history (begin (map-set price-history token-id new-history) true)
      (begin (map-set price-history token-id current-history) true))))

;; ============================================================================
;; Admin Functions
;; ============================================================================

(define-public (set-marketplace-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (var-set marketplace-paused paused)
    (ok paused)))

;; ============================================================================
;; Read-Only Functions
;; ============================================================================

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

(define-read-only (get-offer (token-id uint) (offerer principal))
  (map-get? offers { token-id: token-id, offerer: offerer }))

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

(define-read-only (get-auction (auction-id uint))
  (map-get? auctions auction-id))

(define-read-only (get-bundle (bundle-id uint))
  (map-get? bundles bundle-id))

(define-read-only (get-price-history (token-id uint))
  (default-to (list) (map-get? price-history token-id)))

(define-read-only (get-escrow-balance (user principal))
  (default-to u0 (map-get? escrow-balances user)))

(define-read-only (get-total-volume)
  (var-get total-volume))

(define-read-only (get-total-sales)
  (var-get total-sales))

(define-read-only (get-marketplace-stats)
  {
    total-volume: (var-get total-volume),
    total-sales: (var-get total-sales),
    total-listings: (var-get listing-counter),
    total-auctions: (var-get auction-counter),
    total-bundles: (var-get bundle-counter),
    is-paused: (var-get marketplace-paused)
  })

Functions (29)

FunctionAccessArgs
is-listing-activeprivatetoken-id: uint
is-offer-validprivatetoken-id: uint, offerer: principal
is-auction-activeprivateauction-id: uint
list-nftpublictoken-id: uint, price: uint
list-nft-with-expirypublictoken-id: uint, price: uint, duration: uint
cancel-listingpublictoken-id: uint
update-listing-pricepublictoken-id: uint, new-price: uint
buy-nftpublictoken-id: uint
make-offerpublictoken-id: uint, amount: uint
cancel-offerpublictoken-id: uint
accept-offerpublictoken-id: uint, offerer: principal
create-auctionpublictoken-id: uint, reserve-price: uint, duration: uint
place-bidpublicauction-id: uint, bid-amount: uint
settle-auctionpublicauction-id: uint
create-bundlepublictoken-ids: (list 10 uint
buy-bundlepublicbundle-id: uint
transfer-bundle-nftprivatetoken-id: uint, prev: (response principal uint
record-saleprivatetoken-id: uint, price: uint, buyer: principal
set-marketplace-pausedpublicpaused: bool
get-listingread-onlytoken-id: uint
get-offerread-onlytoken-id: uint, offerer: principal
get-best-offerread-onlytoken-id: uint
get-auctionread-onlyauction-id: uint
get-bundleread-onlybundle-id: uint
get-price-historyread-onlytoken-id: uint
get-escrow-balanceread-onlyuser: principal
get-total-volumeread-only
get-total-salesread-only
get-marketplace-statsread-only