Source Code

;; Block Gallery Marketplace v1
(use-trait nft-trait .sip009-nft-trait.sip009-nft-trait)

;; Errors
(define-constant ERR-UNAUTHORIZED u101)
(define-constant ERR-NOT-FOR-SALE u102)
(define-constant ERR-PRICE-TOO-LOW u103)
(define-constant ERR-NOT-WHITELISTED u104)
(define-constant ERR-HAS-LISTED-ITEMS u105)
(define-constant ERR-MARKET-FROZEN u106)
(define-constant ERR-CONTRACT-FROZEN u107)

;; Constants
(define-constant CONTRACT-OWNER tx-sender)

;; Variables
(define-data-var last-item-id uint u0)
(define-data-var market-frozen bool false) ;; Prevent buy-item, list-item, and update-item from being called
(define-data-var commission-address principal tx-sender)

;; Admins
(define-map market-admins principal bool) ;; Value is permission to set/remove other admins

;; Whitelisted contracts
(define-map market-contracts principal
  {
    frozen: bool,
    royalty: uint,
    commission: uint,
    creator: principal,
    listed-items: uint,
    minimum-price: uint
  }
)

;; Items listed on the market
(define-map market-items { item-id: uint, nft: principal }
  {
    price: uint,
    token-id: uint,
    seller: principal
  }
)

;; Read-only functions
(define-read-only (get-admin (admin principal))
  (map-get? market-admins admin)
)

(define-read-only (get-contract (nft principal))
  (map-get? market-contracts nft)
)

(define-read-only (get-item (item-id uint) (nft principal))
  (map-get? market-items { item-id: item-id, nft: nft })
)

(define-read-only (get-market-frozen)
  (var-get market-frozen)
)

(define-read-only (get-commission-address)
  (var-get commission-address)
)

;; Public functions
(define-public (set-admin (admin principal) (permission bool))
  (begin
    ;; Ensure tx-sender has permission
    (asserts! (is-admin tx-sender true) (err ERR-UNAUTHORIZED))

    ;; Create or update admin
    (map-set market-admins admin permission)

    ;; Print event
    (print {
      action: (if (is-some (get-admin admin)) "update-admin" "new-admin"),
      data: {
        admin: admin,
        permission: permission
      }
    })

    (ok true)
  )
)

(define-public (remove-admin (admin principal))
  (begin
    ;; Ensure tx-sender has permission
    (asserts! (is-admin tx-sender true) (err ERR-UNAUTHORIZED))

    ;; Remove admin
    (map-delete market-admins admin)

    ;; Print event
    (print {
      action: "remove-admin",
      data: {
        admin: admin
      }
    })

    (ok true)
  )
)

(define-public (set-contract (nft principal) (creator principal) (royalty uint) (commission uint) (minimum-price uint) (frozen bool))
  (let (
    (listed-items (default-to u0 (get listed-items (get-contract nft))))
  )
    ;; Ensure tx-sender is an admin
    (asserts! (is-admin tx-sender false) (err ERR-UNAUTHORIZED))

    ;; Create or update collection
    (map-set market-contracts nft
      {
        frozen: frozen,
        royalty: royalty,
        commission: commission,
        creator: creator,
        listed-items: listed-items,
        minimum-price: minimum-price
      }
    )

    ;; Print event
    (print {
      action: (if (is-some (get-contract nft)) "update-contract" "new-contract"),
      data: {
        nft: nft,
        frozen: frozen,
        royalty: royalty,
        commission: commission,
        creator: creator,
        listed-items: listed-items,
        minimum-price: minimum-price
      }
    })

    (ok true)
  )
)

(define-public (remove-contract (nft principal))
  (begin
    ;; Ensure tx-sender is an admin
    (asserts! (is-admin tx-sender false) (err ERR-UNAUTHORIZED))

    ;; Ensure nft is whitelisted
    (unwrap! (get-contract nft) (err ERR-NOT-WHITELISTED))

    ;; Ensure the whitelisted contract has no items listed
    (asserts!
      (is-eq u0 (unwrap-panic (get listed-items (get-contract nft))))
      (err ERR-HAS-LISTED-ITEMS)
    )

    ;; Remove contract
    (map-delete market-contracts nft)

    ;; Print event
    (print {
      action: "remove-contract",
      data: {
        nft: nft
      }
    })

    (ok true)
  )
)

(define-public (list-item (nft <nft-trait>) (token-id uint) (price uint))
  (let (
    (item-id (var-get last-item-id))
    (contract (unwrap! (get-contract (contract-of nft)) (err ERR-NOT-WHITELISTED)))
  )
    ;; Ensure market is not frozen
    (asserts! (not (var-get market-frozen)) (err ERR-MARKET-FROZEN))

    ;; Ensure contract is not frozen
    (asserts! (not (get frozen contract)) (err ERR-CONTRACT-FROZEN))

    ;; Ensure price is greater than or equal to the minimum price
    (asserts! (>= price (get minimum-price contract)) (err ERR-PRICE-TOO-LOW))

    ;; Add item to market
    (map-insert market-items { item-id: item-id, nft: (contract-of nft) }
      {
        price: price,
        token-id: token-id,
        seller: tx-sender
      }
    )

    ;; Increment listed-items on the whitelisted contract by 1
    (map-set market-contracts (contract-of nft)
      (merge contract {
        listed-items: (+ u1 (get listed-items contract))
      })
    )

    ;; Increment last-item-id by 1
    (var-set last-item-id (+ u1 item-id))

    ;; Transfer token from the seller to the contract
    (try!
      (contract-call? nft transfer token-id tx-sender (as-contract tx-sender))
    )

    ;; Print event
    (print {
      action: "list-item",
      data: {
        nft: nft,
        price: price,
        seller: tx-sender,
        item-id: item-id,
        token-id: token-id
      }
    })

    (ok true)
  )
)

(define-public (update-item (nft <nft-trait>) (item-id uint) (price uint))
  (let (
    (item (unwrap!
      (get-item item-id (contract-of nft))
      (err ERR-NOT-FOR-SALE)
    ))
  )
    ;; Ensure market is not frozen
    (asserts! (not (var-get market-frozen)) (err ERR-MARKET-FROZEN))

    ;; Ensure contract is not frozen
    (asserts!
      (not (get frozen (unwrap! (get-contract (contract-of nft)) (err ERR-NOT-WHITELISTED))))
      (err ERR-CONTRACT-FROZEN)
    )

    ;; Ensure tx-sender is the seller of the token
    (asserts! (is-eq tx-sender (get seller item)) (err ERR-UNAUTHORIZED))

    ;; Ensure price is greater than or equal to the minimum price
    (asserts!
      (>= price (unwrap-panic (get minimum-price (get-contract (contract-of nft)))))
      (err ERR-PRICE-TOO-LOW)
    )

    ;; Update item
    (map-set market-items { item-id: item-id, nft: (contract-of nft) }
      (merge item { price: price })
    )

    ;; Print event
    (print {
      action: "update-item",
      data: {
        nft: nft,
        price: price,
        seller: (get seller item),
        item-id: item-id,
        token-id: (get token-id item)
      }
    })

    (ok true)
  )
)

(define-public (unlist-item (nft <nft-trait>) (item-id uint))
  (let (
    (item (unwrap! (get-item item-id (contract-of nft)) (err ERR-NOT-FOR-SALE)))
    (contract (unwrap-panic (get-contract (contract-of nft))))
    (is-seller (is-eq tx-sender (get seller item)))
  )
    ;; Ensure tx-sender is the seller or an admin
    (asserts! (or is-seller (is-admin tx-sender false)) (err ERR-UNAUTHORIZED))

    ;; Remove item
    (map-delete market-items { item-id: item-id, nft: (contract-of nft) })

    ;; Decrement listed-items on the whitelisted contract by 1
    (map-set market-contracts (contract-of nft)
      (merge contract {
        listed-items: (- (get listed-items contract) u1)
      })
    )

    ;; Transfer item from the contract to the seller
    (try!
      ;; Switch contract-call? context to the contract instead of the tx-sender
      (as-contract
        (contract-call? nft transfer (get token-id item) tx-sender (get seller item))
      )
    )

    ;; Print event
    (print {
      action: (if is-seller "seller-unlist-item" "admin-unlist-item"),
      data: {
        nft: nft,
        price: u0,
        seller: (get seller item),
        item-id: item-id,
        token-id: (get token-id item)
      }
    })

    (ok true)
  )
)

(define-public (buy-item (nft <nft-trait>) (item-id uint))
  (let (
    (buyer tx-sender)
    (item (unwrap! (get-item item-id (contract-of nft)) (err ERR-NOT-FOR-SALE)))
    (contract (unwrap-panic (get-contract (contract-of nft))))
    (creator-royalty (calc-fee (get price item) (get royalty contract)))
    (market-commission (calc-fee (get price item) (get commission contract)))
    (total-fees (+ market-commission creator-royalty))
    (profit (- (get price item) (+ market-commission creator-royalty)))
  )
    ;; Ensure market is not frozen
    (asserts! (not (var-get market-frozen)) (err ERR-MARKET-FROZEN))

    ;; Ensure contract is not frozen
    (asserts! (not (get frozen contract)) (err ERR-CONTRACT-FROZEN))

    ;; Transfer market commission if the buyer is not the commission-address
    (and
      (not (is-eq buyer (var-get commission-address)))
      (> market-commission u0)
      (try! (stx-transfer? market-commission buyer (var-get commission-address)))
    )

    ;; Transfer creator royalty if the buyer is not the creator
    (and
      (not (is-eq buyer (get creator contract)))
      (> creator-royalty u0)
      (try! (stx-transfer? creator-royalty buyer (get creator contract)))
    )

    ;; Transfer sale profit if the buyer is not the seller
    (and
      (not (is-eq buyer (get seller item)))
      (> profit u0)
      (try! (stx-transfer? profit buyer (get seller item)))
    )

    ;; Transfer item from the contract to the buyer
    (try!
      ;; Switch contract-call? context to the contract instead of the tx-sender
      (as-contract
        (contract-call? nft transfer (get token-id item) tx-sender buyer)
      )
    )

    ;; Remove item
    (map-delete market-items { item-id: item-id, nft: (contract-of nft) })

    ;; Decrement listed-items on the whitelisted contract by 1
    (map-set market-contracts (contract-of nft)
      (merge contract {
        listed-items: (- (get listed-items contract) u1)
      })
    )

    ;; Print event
    (print {
      action: "buy-item",
      data: {
        nft: nft,
        price: (get price item),
        buyer: buyer,
        seller: (get seller item),
        item-id: item-id,
        token-id: (get token-id item)
      }
    })

    (ok true)
  )
)

(define-public (set-market-frozen (frozen bool))
  (begin
    ;; Ensure tx-sender has permission
    (asserts! (is-admin tx-sender false) (err ERR-UNAUTHORIZED))

    ;; Update market-frozen
    (var-set market-frozen frozen)

    ;; Print event
    (print {
      action: "set-market-frozen",
      data: {
        market-frozen: frozen
      }
    })

    (ok true)
  )
)

(define-public (set-commission-address (address principal))
  (begin
    ;; Ensure tx-sender has permission
    (asserts! (is-admin tx-sender false) (err ERR-UNAUTHORIZED))

    ;; Update commission-address
    (var-set commission-address address)

    ;; Print event
    (print {
      action: "set-commission-address",
      data: {
        address: address
      }
    })

    (ok true)
  )
)

;; Private functions
(define-private (is-admin (address principal) (permission bool))
  (or
    (is-eq address CONTRACT-OWNER)
    ;; Ensure admin has permission to set/remove other admins
    (match (get-admin address) has-permission
      (or (not permission) has-permission)
      false
    )
  )
)

(define-private (calc-fee (price uint) (fee uint))
  ;; Calculate fee for a given price
  ;; (price * fee) / (1 million micro STX)
  (/ (* price fee) u1000000)
)

Functions (17)

FunctionAccessArgs
get-adminread-onlyadmin: principal
get-contractread-onlynft: principal
get-itemread-onlyitem-id: uint, nft: principal
get-market-frozenread-only
get-commission-addressread-only
set-adminpublicadmin: principal, permission: bool
remove-adminpublicadmin: principal
set-contractpublicnft: principal, creator: principal, royalty: uint, commission: uint, minimum-price: uint, frozen: bool
remove-contractpublicnft: principal
list-itempublicnft: <nft-trait>, token-id: uint, price: uint
update-itempublicnft: <nft-trait>, item-id: uint, price: uint
unlist-itempublicnft: <nft-trait>, item-id: uint
buy-itempublicnft: <nft-trait>, item-id: uint
set-market-frozenpublicfrozen: bool
set-commission-addresspublicaddress: principal
is-adminprivateaddress: principal, permission: bool
calc-feeprivateprice: uint, fee: uint