Source Code

;; bitbasel nft
;; contractType: public

(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

(define-non-fungible-token Bitbasel uint)

;; Constants
;;

;; Deployer
(define-constant DEPLOYER tx-sender)

;; Errors
(define-constant ERR-NOT-AUTHORIZED u100)
(define-constant ERR-MINTING-PAUSED u101)
(define-constant ERR-TOKEN-NOT-FOUND u102)
(define-constant ERR-INVALID-ROYALTY-BIPS u103)
(define-constant ERR-LISTING-NOT-FOUND u104)
(define-constant ERR-ROYALTIES-NOT-FOUND u105)
(define-constant ERR-WRONG-COMMISSION u106)

;; Variables & State Management
;;

;; Admins / Contract Owners
(define-map admins principal bool)

;; Global Control
(define-data-var is-minting-paused bool true)

;; Minting Allowlist
(define-map allowlist principal bool)

;; NFT Functional State
(define-data-var last-token-id uint u1)

;; URI i.e. metadata URI
;; Token Minters, i.e. Artists
;; Royalties
(define-map token-metadata uint {
  uri: (string-ascii 256),
  minter: principal,
  royalty: {receiver: principal, bips: uint}
})

;; Assert Conditions
;;
(define-private (is-admin-sender)
  (or (unwrap! (map-get? admins tx-sender) false)
      (unwrap! (map-get? admins contract-caller) false)
  )
)
(define-private (is-minting-active)
  (not (var-get is-minting-paused))
)
(define-private (is-allowlisted-sender)
  (unwrap! (map-get? allowlist tx-sender) false)
)
(define-private (is-token-known (token-id uint))
  (is-some (map-get? token-metadata token-id))
)
(define-private (is-nft-minter-sender (token-id uint))
  (is-eq
    tx-sender
    (get minter (unwrap! (map-get? token-metadata token-id) false))
  )
)
(define-private (is-nft-owner-sender (token-id uint))
  (let (
      (nft-owner (unwrap! (nft-get-owner? Bitbasel token-id) false))
    )
    (or (is-eq tx-sender nft-owner)
        (is-eq contract-caller nft-owner)
    )
  )
)
(define-private (is-nft-royalty-receiver-sender (token-id uint))
  (let (
      (token (unwrap! (map-get? token-metadata token-id) false))
      (royalty-receiver (get receiver (get royalty token)))
    )
    (or (is-eq tx-sender royalty-receiver)
        (is-eq contract-caller royalty-receiver)
    )
  )
)

;; Functions
;;

;; Toggle Paused
(define-public (toggle-minting-paused)
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (var-set is-minting-paused (not (var-get is-minting-paused))))
  )
)

;; Mint
(define-public (mint (uri (string-ascii 256)) (royalty-bips uint))
  (begin
    (asserts! (is-minting-active) (err ERR-MINTING-PAUSED))
    (asserts! (is-allowlisted-sender) (err ERR-NOT-AUTHORIZED))
    (let (
        (minter tx-sender)
        (next-id (+ u1 (var-get last-token-id)))
        (count (var-get last-token-id))
      )
      (match (nft-mint? Bitbasel next-id minter)
        success
        (begin
          (var-set last-token-id next-id)
          (map-set token-metadata next-id {
            uri: uri,
            minter: minter,
            royalty: {receiver: minter, bips: royalty-bips}
          })
          (set-address-allowlisted minter false)
          ;; ;; TODO: Unwrap with error handlers instead of try
          ;; (try! (set-royalty-bips next-id royalty-bips))
          (ok next-id)
        )

        error
        (err error)
      )
    )
  )
)

;; SIP009
;;

;; read-only

;; SIP009: get-last-token-id
(define-read-only (get-last-token-id)
  (ok (var-get last-token-id))
)

;; SIP009: get-token-uri
(define-read-only (get-token-uri (token-id uint))
  (let (
      (token (unwrap!
        (map-get? token-metadata token-id)
        (ok none)
      ))
    )
    (ok (some (get uri token)))
  )
)

;; SIP009
(define-read-only (get-owner (token-id uint))
  (ok (nft-get-owner? Bitbasel token-id))
)

;; public

;; SIP-009: transfer
(define-public
  (transfer
    (token-id uint)
    (sender principal)
    (recipient principal)
  )
  (begin
    (asserts! (is-token-known token-id) (err ERR-TOKEN-NOT-FOUND))
    (asserts! (is-nft-owner-sender token-id) (err ERR-NOT-AUTHORIZED))

    ;; invalidate current listings if the seller transfers the NFT
    (map-delete market token-id)

    (nft-transfer? Bitbasel token-id sender recipient)
  )
)


;; ADMIN LIST FUNCTIONS START
(define-private (set-address-is-admin (address principal) (value bool))
  (map-set admins address value)
)
(define-public (add-address-to-admins (address principal))
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (set-address-is-admin address true))
  )
)
(define-public (remove-address-from-admins (address principal))
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (set-address-is-admin address false))
  )
)
;; ADMIN LIST FUNCTIONS END


;; ALLOWLIST FUNCTIONS START
(define-private (set-address-allowlisted (address principal) (value bool))
  (map-set allowlist address value)
)
(define-public (add-address-to-allowlist (address principal))
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (set-address-allowlisted address true))
  )
)
(define-public (add-address-list-to-allowlist (addresses (list 25 principal)))
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (fold set-address-allowlisted addresses true))
  )
)
(define-public (remove-address-from-allowlist (address principal))
  (begin
    (asserts! (is-admin-sender) (err ERR-NOT-AUTHORIZED))
    (ok (set-address-allowlisted address false))
  )
)
;; ALLOWLIST FUNCTIONS END


;; ROYALTIES FUNCTIONS START
(define-private (is-valid-royalty-bips (royalty-bips uint))
  (and (>= royalty-bips u0) (<= royalty-bips u10000))
)

(define-read-only (get-royalty-bips (token-id uint))
  (let (
      (token (unwrap!
        (map-get? token-metadata token-id)
        (err ERR-TOKEN-NOT-FOUND)
      ))
    )
    (ok (get bips (get royalty token)))
  )
)

(define-public (set-royalty-bips (token-id uint) (royalty-bips uint))
  (let (
      (token (unwrap!
        (map-get? token-metadata token-id)
        (err ERR-TOKEN-NOT-FOUND)
      ))
    )
    (asserts!
      (or (is-nft-royalty-receiver-sender token-id)
          (is-nft-minter-sender token-id)
      )
      (err ERR-NOT-AUTHORIZED)
    )
    (asserts!
      (is-valid-royalty-bips royalty-bips)
      (err ERR-INVALID-ROYALTY-BIPS)
    )
    (let (
        (royalty (get royalty token))
      )
      (ok (map-set token-metadata token-id
        (merge token {
          royalty: (merge royalty {
            bips: royalty-bips
          })
        })
      ))
    )
  )
)
;; ROYALTIES FUNCTIONS END


;; NON-CUSTODIAL FUNCTIONS START

(use-trait commission-trait 'SP1YBE20YMJQRRX26XERR8GA5SPQH938WCG98TST9.bitbasel-traits-v1.commission-trait)

(define-map market uint {
  seller: principal,
  price: uint,
  commission: principal,
  royalty: {receiver: principal, bips: uint}
})

(define-private
  (pay-royalty
    (royalty-receiver principal)
    (royalty-bips uint)
    (price uint)
  )
  (let ((royalty-amount (/ (* price royalty-bips) u10000)))
    (ok (if (> royalty-amount u0)
      ;; TODO: Unwrap with error handlers instead of try
      (try! (stx-transfer? royalty-amount tx-sender royalty-receiver))
      true
    ))
  )
)

;; Non-custodial marketplace transfer
(define-private
  (non-custodial-transfer
    (token-id uint)
    (sender principal)
    (recipient principal)
  )
  (nft-transfer? Bitbasel token-id sender recipient)
)

(define-read-only (get-listing-in-ustx (token-id uint))
  (map-get? market token-id)
)

(define-public (list-in-ustx
  (token-id uint)
  (price uint)
  (comm-trait <commission-trait>)
)
  (let (
      (token
        (unwrap! (map-get? token-metadata token-id) (err ERR-TOKEN-NOT-FOUND))
      )
    )
    (asserts! (is-nft-owner-sender token-id) (err ERR-NOT-AUTHORIZED))
    (let (
        (royalty (get royalty token))
        (royalty-receiver (get receiver royalty))
        (royalty-bips (get bips royalty))
        (listing {
          seller: tx-sender,
          price: price,
          commission: (contract-of comm-trait),
          royalty: {receiver: royalty-receiver, bips: royalty-bips}
        })
      )
      (map-set market token-id listing)
      (ok (print (merge listing {a: "list-in-ustx", token-id: token-id})))
    )
  )
)

(define-public (unlist-in-ustx (token-id uint))
  (begin
    (asserts! (is-token-known token-id) (err ERR-TOKEN-NOT-FOUND))
    (asserts! (is-nft-owner-sender token-id) (err ERR-NOT-AUTHORIZED))
    (asserts! (is-some (map-get? market token-id)) (err ERR-LISTING-NOT-FOUND))
    (map-delete market token-id)
    (ok (print {a: "unlist-in-ustx", token-id: token-id}))
  )
)

(define-public (buy-in-ustx (token-id uint) (comm-trait <commission-trait>))
  (let (
      (nft-owner
        (unwrap!
          (nft-get-owner? Bitbasel token-id)
          (err ERR-TOKEN-NOT-FOUND)
        )
      )
      (listing (unwrap! (map-get? market token-id) (err ERR-LISTING-NOT-FOUND)))
      (seller (get seller listing))
      (price (get price listing))
      (royalty (get royalty listing))
      (royalty-receiver (get receiver royalty))
      (royalty-bips (get bips royalty))
    )
    (asserts!
      (is-eq (contract-of comm-trait) (get commission listing))
      (err ERR-WRONG-COMMISSION)
    )
    ;; TODO: Unwrap with error handlers instead of try
    (try! (stx-transfer? price tx-sender seller))
    (try! (pay-royalty royalty-receiver royalty-bips price))
    (try! (contract-call? comm-trait pay token-id price))
    (try! (non-custodial-transfer token-id seller tx-sender))
    (map-delete market token-id)
    (ok (print {a: "buy-in-ustx", token-id: token-id}))
  )
)
;; NON-CUSTODIAL FUNCTIONS END

;; Initialization
(set-address-is-admin DEPLOYER true)

Functions (25)

FunctionAccessArgs
is-admin-senderprivate
is-minting-activeprivate
is-allowlisted-senderprivate
is-token-knownprivatetoken-id: uint
is-nft-minter-senderprivatetoken-id: uint
is-nft-owner-senderprivatetoken-id: uint
is-nft-royalty-receiver-senderprivatetoken-id: uint
toggle-minting-pausedpublic
mintpublicuri: (string-ascii 256
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
set-address-is-adminprivateaddress: principal, value: bool
add-address-to-adminspublicaddress: principal
remove-address-from-adminspublicaddress: principal
set-address-allowlistedprivateaddress: principal, value: bool
add-address-to-allowlistpublicaddress: principal
add-address-list-to-allowlistpublicaddresses: (list 25 principal
remove-address-from-allowlistpublicaddress: principal
is-valid-royalty-bipsprivateroyalty-bips: uint
get-royalty-bipsread-onlytoken-id: uint
set-royalty-bipspublictoken-id: uint, royalty-bips: uint
get-listing-in-ustxread-onlytoken-id: uint
unlist-in-ustxpublictoken-id: uint
buy-in-ustxpublictoken-id: uint, comm-trait: <commission-trait>