Source Code

;; title: badge-nft
;; summary: SIP-009 compliant NFT used as quest badges
;; note: built with explicit admin/minter roles and token-level URIs

;; token definition
(define-non-fungible-token badge uint)

;; constants
(define-constant err-not-owner (err u100))
(define-constant err-not-minter (err u101))
(define-constant err-uri-required (err u102))
(define-constant err-supply-exceeded (err u103))
(define-constant err-token-exists (err u104))
(define-constant err-not-token-owner (err u105))
(define-constant err-token-not-found (err u106))
(define-constant err-max-supply-too-low (err u107))
(define-constant err-mint-paused (err u108))
(define-constant err-invalid-recipient (err u109))
(define-constant err-metadata-locked (err u110))
(define-constant err-burn-disabled (err u111))
(define-constant contract-version u1)
(define-constant contract-owner tx-sender)
(define-data-var contract-admin principal contract-owner)
(define-data-var authorized-minter principal contract-owner)
(define-data-var base-uri (optional (string-utf8 256)) none)
(define-data-var max-supply (optional uint) none)
(define-data-var last-token-id uint u0)
(define-data-var total-supply uint u0)
(define-data-var mint-paused bool false)
(define-data-var metadata-locked bool false)
(define-data-var burn-enabled bool false)
(define-data-var collection-name (string-utf8 64) u"Quest Badge")
(define-data-var collection-symbol (string-utf8 32) u"QBADGE")
(define-map token-uri { id: uint } { uri: (string-utf8 256) })
(define-map token-minter { id: uint } { minter: principal })
(define-map token-minted-at { id: uint } { minted-at: uint })
(define-private (is-owner (who principal))
  (is-eq who (var-get contract-admin)))
(define-private (is-minter (who principal))
  (or (is-eq who (var-get authorized-minter))
      (is-eq contract-caller (var-get authorized-minter))))
(define-private (assert-owner (who principal))
  (is-owner who))
(define-private (assert-minter (who principal))
  (is-minter who))
(define-private (assert-valid-recipient (who principal))
  (not (is-eq who 'SP000000000000000000002Q6VF78)))
(define-private (ensure-uri (uri (string-utf8 256)))
  (> (len uri) u0))
(define-private (next-token-id)
  (let ((new-id (+ (var-get last-token-id) u1)))
    (begin
      (var-set last-token-id new-id)
      new-id)))
(define-private (increment-supply)
  (var-set total-supply (+ (var-get total-supply) u1)))
(define-private (decrement-supply)
  (var-set total-supply (- (var-get total-supply) u1)))
(define-private (enforce-supply-limit)
  (match (var-get max-supply)
    max (<= (+ (var-get total-supply) u1) max)
    true))
(define-private (set-uri! (id uint) (uri (string-utf8 256)))
  (map-set token-uri { id: id } { uri: uri }))
(define-private (clear-uri! (id uint))
  (map-delete token-uri { id: id }))
(define-private (record-mint (recipient principal) (token-id uint) (uri (string-utf8 256)))
  (begin
    (asserts! (enforce-supply-limit) err-supply-exceeded)
    (asserts! (not (is-some (nft-get-owner? badge token-id))) err-token-exists)
    (match (nft-mint? badge token-id recipient)
      minted
        (begin
          (set-uri! token-id uri)
          (map-set token-minter { id: token-id } { minter: tx-sender })
          (map-set token-minted-at { id: token-id } { minted-at: stacks-block-time })
          (increment-supply)
          (ok token-id))
      mint-err (err mint-err))))
(define-private (apply-base-uri (uri (string-utf8 256)))
  (match (var-get base-uri)
    base (concat base uri)
    uri))

;; admin: update authorized minter
(define-public (set-minter (minter principal))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (assert-valid-recipient minter) err-invalid-recipient)
    (var-set authorized-minter minter)
    (print { event: "set-minter", minter: minter })
    (ok true)))

;; admin: transfer admin role
(define-public (set-admin (new-admin principal))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (assert-valid-recipient new-admin) err-invalid-recipient)
    (var-set contract-admin new-admin)
    (print { event: "set-admin", admin: new-admin })
    (ok true)))

;; admin: update base URI
(define-public (set-base-uri (uri (string-utf8 256)))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (ensure-uri uri) err-uri-required)
    (var-set base-uri (some uri))
    (print { event: "set-base-uri", uri: uri })
    (ok true)))

;; admin: set max supply (cannot be below current supply)
(define-public (set-max-supply (limit uint))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (>= limit (var-get total-supply)) err-max-supply-too-low)
    (var-set max-supply (some limit))
    (print { event: "set-max-supply", limit: limit })
    (ok true)))

;; admin: remove max supply limit
(define-public (clear-max-supply)
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (var-set max-supply none)
    (print { event: "clear-max-supply" })
    (ok true)))

;; admin: pause/unpause minting
(define-public (set-mint-paused (flag bool))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (var-set mint-paused flag)
    (print { event: "set-mint-paused", paused: flag })
    (ok true)))

;; admin: toggle burn ability
(define-public (set-burn-enabled (flag bool))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (var-set burn-enabled flag)
    (print { event: "set-burn-enabled", enabled: flag })
    (ok true)))

;; admin: clear base uri
(define-public (clear-base-uri)
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (var-set base-uri none)
    (print { event: "clear-base-uri" })
    (ok true)))

;; read: current minter
(define-read-only (get-minter)
  (ok (var-get authorized-minter)))

;; read: base uri
(define-read-only (get-base-uri)
  (ok (var-get base-uri)))

;; read: max supply
(define-read-only (get-max-supply)
  (ok (var-get max-supply)))

;; read: total supply
(define-read-only (get-total-supply)
  (ok (var-get total-supply)))

;; read: remaining supply (if capped)
(define-read-only (get-remaining-supply)
  (match (var-get max-supply)
    max (ok (some (- max (var-get total-supply))))
    (ok none)))

;; read: mint pause state
(define-read-only (is-mint-paused)
  (ok (var-get mint-paused)))

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

;; read: next token id
(define-read-only (get-next-token-id)
  (ok (+ (var-get last-token-id) u1)))

;; read: token uri
(define-read-only (get-token-uri (id uint))
  (match (map-get? token-uri { id: id })
    entry (ok (apply-base-uri (get uri entry)))
    err-token-not-found))

;; read: raw token uri (without base)
(define-read-only (get-token-uri-raw (id uint))
  (match (map-get? token-uri { id: id })
    entry (ok (get uri entry))
    err-token-not-found))

;; read: token minter
(define-read-only (get-token-minter (id uint))
  (get-token-minter-or-err id))

(define-private (get-token-minted-at-or-err (id uint))
  (match (map-get? token-minted-at { id: id })
    entry (ok (get minted-at entry))
    err-token-not-found))

(define-read-only (get-token-minted-at (id uint))
  (get-token-minted-at-or-err id))

(define-private (get-token-minter-or-err (id uint))
  (match (map-get? token-minter { id: id })
    entry (ok (get minter entry))
    err-token-not-found))

;; read: bundled token info
(define-read-only (get-token-info (id uint))
  (let (
        (owner (unwrap! (get-owner-or-err id) err-token-not-found))
        (uri (unwrap! (get-token-uri-raw id) err-token-not-found))
        (minter (unwrap! (get-token-minter-or-err id) err-token-not-found))
        (minted-at (unwrap! (get-token-minted-at-or-err id) err-token-not-found)))
    (ok { owner: owner, uri: uri, minter: minter, minted-at: minted-at })))

;; read: collection name
(define-read-only (get-name)
  (ok (var-get collection-name)))

;; read: collection symbol
(define-read-only (get-symbol)
  (ok (var-get collection-symbol)))

;; read: token owner
(define-read-only (get-owner (id uint))
  (match (nft-get-owner? badge id)
    owner (ok owner)
    err-token-not-found))
(define-private (token-exists? (id uint))
  (is-some (nft-get-owner? badge id)))
(define-private (get-owner-or-err (id uint))
  (match (nft-get-owner? badge id)
    owner (ok owner)
    err-token-not-found))

;; admin: update token uri
(define-public (set-token-uri (id uint) (uri (string-utf8 256)))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (not (var-get metadata-locked)) err-metadata-locked)
    (asserts! (ensure-uri uri) err-uri-required)
    (asserts! (token-exists? id) err-token-not-found)
    (set-uri! id uri)
    (print { event: "set-token-uri", id: id, uri: uri })
    (ok true)))

;; admin: update collection name
(define-public (set-name (name (string-utf8 64)))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (not (var-get metadata-locked)) err-metadata-locked)
    (asserts! (> (len name) u0) err-uri-required)
    (var-set collection-name name)
    (print { event: "set-name", name: name })
    (ok true)))

;; admin: update collection symbol
(define-public (set-symbol (symbol (string-utf8 32)))
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (asserts! (not (var-get metadata-locked)) err-metadata-locked)
    (asserts! (> (len symbol) u0) err-uri-required)
    (var-set collection-symbol symbol)
    (print { event: "set-symbol", symbol: symbol })
    (ok true)))

;; admin: lock metadata updates permanently
(define-public (lock-metadata)
  (begin
    (asserts! (assert-owner tx-sender) err-not-owner)
    (var-set metadata-locked true)
    (print { event: "lock-metadata" })
    (ok true)))

;; public: transfer token
(define-public (transfer (token-id uint) (sender principal) (recipient principal))
  (let ((owner (unwrap! (get-owner-or-err token-id) err-token-not-found)))
    (asserts! (is-eq owner sender) err-not-token-owner)
    (asserts! (assert-valid-recipient recipient) err-invalid-recipient)
    (let ((res (nft-transfer? badge token-id sender recipient)))
      (match res
        ok (begin (print { event: "transfer", id: token-id, from: sender, to: recipient }) res)
        err res))))

;; public: mint new badge (minter only)
(define-public (mint (recipient principal) (uri (string-utf8 256)))
  (begin
    (asserts! (assert-minter tx-sender) err-not-minter)
    (asserts! (not (var-get mint-paused)) err-mint-paused)
    (asserts! (assert-valid-recipient recipient) err-invalid-recipient)
    (asserts! (ensure-uri uri) err-uri-required)
    (let ((new-id (next-token-id)))
      (let ((result (record-mint recipient new-id uri)))
        (match result
          id (begin (print { event: "mint", id: id, to: recipient, uri: uri }) result)
          err result)))))

;; public: burn token (owner only, if enabled)
(define-public (burn (token-id uint))
  (let ((owner (unwrap! (get-owner-or-err token-id) err-token-not-found)))
    (asserts! (is-eq true (var-get burn-enabled)) err-burn-disabled)
    (asserts! (is-eq owner tx-sender) err-not-token-owner)
    (let ((burn-res (nft-burn? badge token-id owner)))
      (match burn-res
        burn-ok (begin
                  (clear-uri! token-id)
                  (map-delete token-minter { id: token-id })
                  (map-delete token-minted-at { id: token-id })
                  (decrement-supply)
                  (print { event: "burn", id: token-id, by: tx-sender })
                  burn-res)
        burn-err burn-res))))

;; read: can mint now?
(define-read-only (can-mint)
  (ok (and (not (var-get mint-paused))
           (match (var-get max-supply)
             max (< (var-get total-supply) max)
             true))))

;; read: token exists?
(define-read-only (token-exists (id uint))
  (ok (token-exists? id)))

;; read: metadata lock status
(define-read-only (is-metadata-locked)
  (ok (var-get metadata-locked)))

;; read: burn enabled?
(define-read-only (is-burn-enabled)
  (ok (var-get burn-enabled)))

;; read: contract owner
(define-read-only (get-owner-principal)
  (ok (var-get contract-admin)))

;; read: contract version
(define-read-only (get-version)
  (ok contract-version))

Functions (55)

FunctionAccessArgs
get-token-uri-rawread-onlyid: uint
is-ownerprivatewho: principal
is-minterprivatewho: principal
assert-ownerprivatewho: principal
assert-minterprivatewho: principal
assert-valid-recipientprivatewho: principal
ensure-uriprivateuri: (string-utf8 256
next-token-idprivate
increment-supplyprivate
decrement-supplyprivate
enforce-supply-limitprivate
set-uri!privateid: uint, uri: (string-utf8 256
clear-uri!privateid: uint
record-mintprivaterecipient: principal, token-id: uint, uri: (string-utf8 256
apply-base-uriprivateuri: (string-utf8 256
set-minterpublicminter: principal
set-adminpublicnew-admin: principal
set-base-uripublicuri: (string-utf8 256
set-max-supplypubliclimit: uint
clear-max-supplypublic
set-mint-pausedpublicflag: bool
set-burn-enabledpublicflag: bool
clear-base-uripublic
get-minterread-only
get-base-uriread-only
get-max-supplyread-only
get-total-supplyread-only
get-remaining-supplyread-only
is-mint-pausedread-only
get-last-token-idread-only
get-next-token-idread-only
get-token-uriread-onlyid: uint
get-token-minterread-onlyid: uint
get-token-minted-at-or-errprivateid: uint
get-token-minted-atread-onlyid: uint
get-token-minter-or-errprivateid: uint
get-token-inforead-onlyid: uint
get-nameread-only
get-symbolread-only
get-ownerread-onlyid: uint
token-exists?privateid: uint
get-owner-or-errprivateid: uint
set-token-uripublicid: uint, uri: (string-utf8 256
set-namepublicname: (string-utf8 64
set-symbolpublicsymbol: (string-utf8 32
lock-metadatapublic
transferpublictoken-id: uint, sender: principal, recipient: principal
mintpublicrecipient: principal, uri: (string-utf8 256
burnpublictoken-id: uint
can-mintread-only
token-existsread-onlyid: uint
is-metadata-lockedread-only
is-burn-enabledread-only
get-owner-principalread-only
get-versionread-only