Source Code

;; title: metaboys
;; version: 1
;; summary: Burn a MetaBoy Cartridge to Mint a Stacks MetaBoy!
;; description: Aside from the Burn to Reveal aspect, this contract follows standard conventions of NFT's and non-custodial marketplaces

;; *** CHANGE ALL .cartridges to Wontons actual Cartridge contract

;; Network NFT trait
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
;; Network commission trait
(use-trait commission-trait 'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.commission-trait.commission) 

;; Defining NFT
(define-non-fungible-token metaboys uint)

;; Storage
;; Keeping track of token count for each principal
(define-map token-count principal uint)
;; Keeping track of non-custodial market listings
(define-map market uint {price: uint, commission: principal, royalty: uint})

;; Constants
(define-constant ERR-WRONG-COMMISSION (err u301))
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-NOT-FOUND (err u404))
(define-constant ERR-INVALID-USER (err u405))
(define-constant ERR-SAME-VALUE (err u500))
(define-constant ERR-CANT-GET-OWNER (err u504))
(define-constant ERR-METADATA-FROZEN (err u505))
(define-constant ERR-LISTING (err u507))
(define-constant ERR-INVALID-PERCENTAGE (err u514))
(define-constant ERR-CANT-BURN (err u600))
(define-constant ERR-STX-TRANSFER (err u601))
(define-constant ERR-PAY-ROYALTY (err u602))
(define-constant ERR-CANT-TRANSFER-NFT (err u603))
(define-constant ERR-MARKETPLACE-ROYALTY (err u604))
(define-constant ERR-BURN-FAILED (err u10000))

;; Internal variables
;; Deployer
(define-data-var CONTRACT-OWNER principal tx-sender)
;; Index counter
(define-data-var last-id uint u1)
;; Metadata freezer
(define-data-var metadata-frozen bool false) 
;; Base URI for token metadata
(define-data-var ipfs-root (string-ascii 100) "ipfs://ipfs/QmUgXA2zuJhvgJy5Q1ycsx3LQ1mffHySDrfVCSy5fk8WZQ/json/")
;; Team royalty percentage per sale (0.5%)
(define-data-var royalty-percent uint u500)

;; NFT traits - SIP009
;; SIP009: Transfer token to a specified principal
(define-public (transfer (id uint) (sender principal) (recipient principal))
  (begin
    ;; Asserting that sender owns token -> not needed as nft-transfer? implicitly checks this but nice to have anyways
    (asserts! (is-eq (unwrap! (unwrap! (get-owner id) ERR-CANT-GET-OWNER) ERR-CANT-GET-OWNER) recipient) ERR-NOT-AUTHORIZED) ;; Check if sender owns token
    ;; Asserting that tx-sender is sender from params
    (asserts! (is-eq tx-sender sender) ERR-NOT-AUTHORIZED) ;; Check if sender is actually the sender
    ;; Asserting that listing is empty
    (asserts! (is-none (map-get? market id)) ERR-LISTING)
    (trnsfr id sender recipient)))

;; SIP009: Get the owner of the specified token ID
(define-read-only (get-owner (id uint))
  (ok (nft-get-owner? metaboys id)))

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

;; SIP009: Get the token URI.
(define-read-only (get-token-uri (token-id uint))
  (ok (some (concat (concat (var-get ipfs-root) "{id}") ".json"))))

;; Set the base of the uri that prepends the token uri.
(define-public (set-base-uri (new-base-uri (string-ascii 100)))
  (begin
    ;; Asserting new value isn't same value
    (asserts! (not (is-eq new-base-uri (var-get ipfs-root))) (err ERR-SAME-VALUE))
    ;; Asserting tx-sender is contract owner / admin
    (asserts! (is-eq tx-sender (var-get CONTRACT-OWNER)) (err ERR-NOT-AUTHORIZED)) ;; Must be contract owner
    ;; Asserting that metadata is not frozen
    (asserts! (not (var-get metadata-frozen)) (err ERR-METADATA-FROZEN))
    (print { notification: "token-metadata-update", payload: { token-class: "nft", contract-id: (as-contract tx-sender) }})
    ;; Update IPFS root
    (var-set ipfs-root new-base-uri)
    (ok true)))

;; Change the contract owner 
(define-public (change-owner (address principal))
  (begin
    ;; Asserting new value isn't same value
    (asserts! (not (is-eq tx-sender address)) ERR-SAME-VALUE)
    ;; Asserting tx-sender is contract owner / admin
    (asserts! (is-eq tx-sender (var-get CONTRACT-OWNER)) ERR-NOT-AUTHORIZED)
    ;; Update contract owner
    (var-set CONTRACT-OWNER address)
    (ok true)))

;; Freeze metadata
(define-public (freeze-metadata)
  (begin
    ;; Asserting tx-sender is contract owner / admin
    (asserts! (is-eq tx-sender (var-get CONTRACT-OWNER)) ERR-NOT-AUTHORIZED)
    ;; Update metadata frozen
    (var-set metadata-frozen true)
    (ok true)))

;; Token count for account
(define-read-only (get-balance (account principal))
  (default-to u0
    (map-get? token-count account)))

;; Does the actual transfer, checks are handled by calling function
(define-private (trnsfr (id uint) (sender principal) (recipient principal))
  (match (nft-transfer? metaboys id sender recipient) ;;Transfer NFT from sender to recipient
    success
      (begin
          (map-set token-count ;;Decrease sender balance
                sender
                (- (get-balance sender) u1))
          (map-set token-count ;;Increase recipient balance
                recipient
                (+ (get-balance recipient) u1))
          (ok success)
      )
    error (err error)))

;; Burn to Reveal mint. Check if sender has a Cartridge from the original mint contract. Burn Cartridge, mint Meta Boy
(define-public (mint (id uint))
  (begin
    (asserts! (is-eq (unwrap! (unwrap! (contract-call? 'SP358JBH8FRAF33V83010X1FAFFRK7W3ANJY6HPQ7.numbers get-owner id) ERR-INVALID-USER) ERR-INVALID-USER) tx-sender) ERR-NOT-AUTHORIZED)
    (match (nft-mint? metaboys id tx-sender) ;; MINT
      success
        (begin
          (unwrap! (contract-call? 'SP358JBH8FRAF33V83010X1FAFFRK7W3ANJY6HPQ7.numbers burn id) ERR-CANT-BURN)
          (var-set last-id (+ u1 (var-get last-id)))
          (map-set token-count ;; Increase the new-owner NFT count
            tx-sender
            (+ (get-balance tx-sender) u1)
          )
          (ok true))
      error (err (* error u10000))
    )
  )
)

;; Iterate through, and mint, the list of ids
(define-private (mint-many-iter (id uint) )
    (begin
      (asserts! (is-eq (unwrap! (unwrap! (contract-call? 'SP358JBH8FRAF33V83010X1FAFFRK7W3ANJY6HPQ7.numbers get-owner id) ERR-INVALID-USER) ERR-INVALID-USER) tx-sender) ERR-NOT-AUTHORIZED)
      (match (nft-mint? metaboys id tx-sender)
        success
            (begin
              (unwrap! (contract-call? 'SP358JBH8FRAF33V83010X1FAFFRK7W3ANJY6HPQ7.numbers burn id) ERR-CANT-BURN)
              (var-set last-id (+ u1 (var-get last-id)))
              (map-set token-count
                tx-sender
                (+ (get-balance tx-sender) u1)
              )
              (ok true)
            )
        error (err (* error u10000)))
    )
)

;; Mint many NFTs, through array of ids
(define-public (mint-many (ids (list 50 uint)))
  (begin   
    (print (map mint-many-iter ids)) 
    (ok true)
  ))

;; Burn the NFT, by received id
(define-public (burn (id uint))
    (let (
      (token-owner (unwrap! (unwrap! (get-owner id) ERR-CANT-GET-OWNER) ERR-CANT-GET-OWNER)) 
    )
    (asserts! (is-eq tx-sender token-owner) ERR-NOT-AUTHORIZED)
    (asserts! (is-none (map-get? market id)) ERR-LISTING)
    (match (nft-burn? metaboys id token-owner)
        success
        (let
        ((current-balance (get-balance token-owner)))
          (begin
            (map-set token-count
              token-owner
              (- current-balance u1)
            )
            (ok true)))
        error (err (* error u10000)))
    )
)

;; Non-custodial marketplace extras

;; Check if the tx-sender is the owner of this NFT id
(define-private (is-sender-owner (id uint))
  (let ((owner (unwrap! (nft-get-owner? metaboys id) false)))
    (or (is-eq tx-sender owner) (is-eq contract-caller owner))))

;; Get the NFT listing, in micro stacks
(define-read-only (get-listing-in-ustx (id uint))
  (map-get? market id))

;; List an NFT, in micro stacks
(define-public (list-in-ustx (id uint) (price uint) (comm-trait <commission-trait>))
  (let ((listing  {price: price, commission: (contract-of comm-trait), royalty: (var-get royalty-percent)}))
    (asserts! (is-sender-owner id) (err ERR-NOT-AUTHORIZED))
    (map-set market id listing)
    (print (merge listing {a: "list-in-ustx", id: id}))
    (ok true)))

;; Remove listing of an NFT
(define-public (unlist-in-ustx (id uint))
  (begin
    (asserts! (is-sender-owner id) (err ERR-NOT-AUTHORIZED))
    (map-delete market id)
    (print {a: "unlist-in-ustx", id: id})
    (ok true)))


;; Buy an NFT! In micro stacks
(define-public (buy-in-ustx (id uint) (comm-trait <commission-trait>))
  (let ((owner (unwrap! (nft-get-owner? metaboys id) ERR-NOT-FOUND))
      (listing (unwrap! (map-get? market id) ERR-LISTING))
      (price (get price listing))
      (royalty (get royalty listing)))
    (asserts! (is-eq (contract-of comm-trait) (get commission listing)) ERR-WRONG-COMMISSION)
    (unwrap! (stx-transfer? price tx-sender owner) ERR-STX-TRANSFER)
    (unwrap! (pay-royalty price royalty) ERR-PAY-ROYALTY) 
    (unwrap! (contract-call? comm-trait pay id price) ERR-MARKETPLACE-ROYALTY)
    (unwrap! (trnsfr id owner tx-sender) ERR-CANT-TRANSFER-NFT)
    (map-delete market id)
    (print {a: "buy-in-ustx", id: id})
    (ok true)))
    
(define-read-only (get-royalty-percent)
  (ok (var-get royalty-percent)))

;; Set the royalty percentage to the received value
(define-public (set-royalty-percent (royalty uint))
  (begin
    (asserts! (is-eq tx-sender (var-get CONTRACT-OWNER)) (err ERR-INVALID-USER))
    (asserts! (and (>= royalty u0) (<= royalty u1000)) (err ERR-INVALID-PERCENTAGE))
    (ok (var-set royalty-percent royalty))))


(define-private (pay-royalty (price uint) (royalty uint))
  (let (
    (royalty-amount (/ (* price royalty) u10000))
  )
  (if (and (> royalty-amount u0) (not (is-eq tx-sender (var-get CONTRACT-OWNER))))
    (unwrap! (stx-transfer? royalty-amount tx-sender (var-get CONTRACT-OWNER)) ERR-PAY-ROYALTY)
    (print false)
  )
  (ok true)))

Functions (20)

FunctionAccessArgs
transferpublicid: uint, sender: principal, recipient: principal
get-ownerread-onlyid: uint
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
set-base-uripublicnew-base-uri: (string-ascii 100
change-ownerpublicaddress: principal
freeze-metadatapublic
get-balanceread-onlyaccount: principal
trnsfrprivateid: uint, sender: principal, recipient: principal
mintpublicid: uint
mint-manypublicids: (list 50 uint
burnpublicid: uint
is-sender-ownerprivateid: uint
get-listing-in-ustxread-onlyid: uint
list-in-ustxpublicid: uint, price: uint, comm-trait: <commission-trait>
unlist-in-ustxpublicid: uint
buy-in-ustxpublicid: uint, comm-trait: <commission-trait>
get-royalty-percentread-only
set-royalty-percentpublicroyalty: uint
pay-royaltyprivateprice: uint, royalty: uint