Source Code

;; StacksMint NFT Contract v2.1
;; SIP-009 compliant NFT with batch minting, royalties, metadata, and delegation
;; Version: 2.1.0
;; Author: StacksMint Team
;; Upgrade: v2.1 - Delegation, reveal mechanism, rarity traits, bulk operations

(impl-trait 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.sip009-nft-trait.nft-trait)

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

(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_OWNER (err u100))
(define-constant ERR_NOT_FOUND (err u101))
(define-constant ERR_ALREADY_MINTED (err u102))
(define-constant ERR_INVALID_URI (err u103))
(define-constant ERR_NOT_AUTHORIZED (err u104))
(define-constant ERR_TOKEN_FROZEN (err u105))
(define-constant ERR_BATCH_LIMIT_EXCEEDED (err u106))
(define-constant ERR_INVALID_ROYALTY (err u107))
(define-constant ERR_NOT_REVEALED (err u108))
(define-constant ERR_ALREADY_REVEALED (err u109))
(define-constant ERR_DELEGATE_EXISTS (err u110))
(define-constant ERR_NOT_DELEGATE (err u111))
(define-constant ERR_INVALID_RARITY (err u112))

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

(define-constant MAX_BATCH_SIZE u25)
(define-constant MAX_ROYALTY_PERCENT u100) ;; 10% in basis points / 10

;; Rarity tiers
(define-constant RARITY_COMMON u1)
(define-constant RARITY_UNCOMMON u2)
(define-constant RARITY_RARE u3)
(define-constant RARITY_EPIC u4)
(define-constant RARITY_LEGENDARY u5)

;; ============================================================================
;; NFT Definition
;; ============================================================================

(define-non-fungible-token stacksmint-nft-v2-1-3 uint)

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

(define-data-var last-token-id uint u0)
(define-data-var total-burned uint u0)
(define-data-var minting-paused bool false)
(define-data-var reveal-block uint u0)
(define-data-var placeholder-uri (string-ascii 256) "ipfs://placeholder")

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

;; Token URI storage
(define-map token-uris uint (string-ascii 256))

;; Pre-reveal URIs (stored before reveal)
(define-map pre-reveal-uris uint (string-ascii 256))

;; Token metadata (creator, royalty, etc.)
(define-map token-metadata uint {
  creator: principal,
  royalty-percent: uint,
  created-at: uint,
  collection-id: (optional uint),
  rarity: uint,
  revealed: bool
})

;; Frozen tokens (cannot be transferred)
(define-map frozen-tokens uint bool)

;; Approved operators for transfers
(define-map token-approvals uint principal)

;; Operator approvals (approve all)
(define-map operator-approvals { owner: principal, operator: principal } bool)

;; Delegation system
(define-map delegates { owner: principal, delegate: principal } {
  can-mint: bool,
  can-transfer: bool,
  can-list: bool,
  expires-at: uint
})

;; Token traits for rarity calculation
(define-map token-traits uint (list 10 { trait-type: (string-ascii 32), value: (string-ascii 64) }))

;; Rarity scores
(define-map rarity-scores uint uint)

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

(define-private (is-owner-or-approved (token-id uint) (sender principal))
  (let ((owner (unwrap! (nft-get-owner? stacksmint-nft-v2-1-3 token-id) false)))
    (or 
      (is-eq sender owner)
      (is-eq (some sender) (map-get? token-approvals token-id))
      (default-to false (map-get? operator-approvals { owner: owner, operator: sender }))
      (is-valid-delegate owner sender))))

(define-private (is-token-frozen (token-id uint))
  (default-to false (map-get? frozen-tokens token-id)))

(define-private (is-valid-delegate (owner principal) (delegate principal))
  (match (map-get? delegates { owner: owner, delegate: delegate })
    del (and (< block-height (get expires-at del)) (get can-transfer del))
    false))

(define-private (can-delegate-mint (owner principal) (delegate principal))
  (match (map-get? delegates { owner: owner, delegate: delegate })
    del (and (< block-height (get expires-at del)) (get can-mint del))
    false))

;; ============================================================================
;; Minting Functions
;; ============================================================================

;; Mint single NFT
(define-public (mint (uri (string-ascii 256)))
  (mint-with-royalty uri u0))

;; Mint with royalty percentage
(define-public (mint-with-royalty (uri (string-ascii 256)) (royalty-percent uint))
  (mint-full uri royalty-percent RARITY_COMMON none))

;; Full mint with all options
(define-public (mint-full 
  (uri (string-ascii 256)) 
  (royalty-percent uint) 
  (rarity uint)
  (collection-id (optional uint)))
  (begin
    (asserts! (not (var-get minting-paused)) ERR_NOT_AUTHORIZED)
    (asserts! (<= royalty-percent MAX_ROYALTY_PERCENT) ERR_INVALID_ROYALTY)
    (asserts! (and (>= rarity RARITY_COMMON) (<= rarity RARITY_LEGENDARY)) ERR_INVALID_RARITY)
    (let ((token-id (+ (var-get last-token-id) u1))
          (is-revealed (is-eq (var-get reveal-block) u0)))
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      (try! (nft-mint? stacksmint-nft-v2-1-3 token-id tx-sender))
      
      ;; Store URI based on reveal status
      (if is-revealed
        (map-set token-uris token-id uri)
        (begin
          (map-set pre-reveal-uris token-id uri)
          (map-set token-uris token-id (var-get placeholder-uri))))
      
      (map-set token-metadata token-id {
        creator: tx-sender,
        royalty-percent: royalty-percent,
        created-at: block-height,
        collection-id: collection-id,
        rarity: rarity,
        revealed: is-revealed
      })
      (map-set rarity-scores token-id (* rarity u100))
      (var-set last-token-id token-id)
      (ok token-id))))

;; Batch mint multiple NFTs
(define-public (batch-mint (uris (list 25 (string-ascii 256))))
  (let ((count (len uris)))
    (asserts! (<= count MAX_BATCH_SIZE) ERR_BATCH_LIMIT_EXCEEDED)
    (fold batch-mint-iter uris (ok u0))))

(define-private (batch-mint-iter (uri (string-ascii 256)) (prev (response uint uint)))
  (match prev
    success (match (mint uri)
      token-id (ok token-id)
      err prev)
    err prev))

;; ============================================================================
;; Delegation Functions
;; ============================================================================

;; Add delegate with permissions
(define-public (add-delegate 
  (delegate principal)
  (can-mint bool)
  (can-transfer bool)
  (can-list bool)
  (duration uint))
  (begin
    (asserts! (is-none (map-get? delegates { owner: tx-sender, delegate: delegate })) ERR_DELEGATE_EXISTS)
    (map-set delegates { owner: tx-sender, delegate: delegate } {
      can-mint: can-mint,
      can-transfer: can-transfer,
      can-list: can-list,
      expires-at: (+ block-height duration)
    })
    (ok true)))

;; Remove delegate
(define-public (remove-delegate (delegate principal))
  (begin
    (map-delete delegates { owner: tx-sender, delegate: delegate })
    (ok true)))

;; Mint on behalf of owner (as delegate)
(define-public (mint-as-delegate (owner principal) (uri (string-ascii 256)))
  (begin
    (asserts! (can-delegate-mint owner tx-sender) ERR_NOT_DELEGATE)
    (let ((token-id (+ (var-get last-token-id) u1)))
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      (try! (nft-mint? stacksmint-nft-v2-1-3 token-id owner))
      (map-set token-uris token-id uri)
      (map-set token-metadata token-id {
        creator: owner,
        royalty-percent: u0,
        created-at: block-height,
        collection-id: none,
        rarity: RARITY_COMMON,
        revealed: true
      })
      (var-set last-token-id token-id)
      (ok token-id))))

;; ============================================================================
;; Reveal Mechanism
;; ============================================================================

;; Set reveal block (owner only)
(define-public (set-reveal-block (reveal-at uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (var-set reveal-block reveal-at)
    (ok reveal-at)))

;; Reveal token (after reveal block)
(define-public (reveal-token (token-id uint))
  (let ((metadata (unwrap! (map-get? token-metadata token-id) ERR_NOT_FOUND))
        (pre-uri (unwrap! (map-get? pre-reveal-uris token-id) ERR_ALREADY_REVEALED)))
    (asserts! (>= block-height (var-get reveal-block)) ERR_NOT_REVEALED)
    (asserts! (not (get revealed metadata)) ERR_ALREADY_REVEALED)
    (map-set token-uris token-id pre-uri)
    (map-set token-metadata token-id (merge metadata { revealed: true }))
    (map-delete pre-reveal-uris token-id)
    (ok true)))

;; Batch reveal tokens
(define-public (batch-reveal (token-ids (list 25 uint)))
  (fold batch-reveal-iter token-ids (ok u0)))

(define-private (batch-reveal-iter (token-id uint) (prev (response uint uint)))
  (match prev
    success (match (reveal-token token-id)
      result (ok (+ success u1))
      err prev)
    err prev))

;; ============================================================================
;; Trait Functions
;; ============================================================================

;; Set token traits
(define-public (set-token-traits 
  (token-id uint) 
  (traits (list 10 { trait-type: (string-ascii 32), value: (string-ascii 64) })))
  (begin
    (asserts! (is-owner-or-approved token-id tx-sender) ERR_NOT_AUTHORIZED)
    (map-set token-traits token-id traits)
    (ok true)))

;; Calculate and set rarity score based on traits
(define-public (calculate-rarity-score (token-id uint) (score uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (map-set rarity-scores token-id score)
    (ok score)))

;; ============================================================================
;; Transfer Functions
;; ============================================================================

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
  (begin
    (asserts! (is-owner-or-approved token-id tx-sender) ERR_NOT_AUTHORIZED)
    (asserts! (not (is-token-frozen token-id)) ERR_TOKEN_FROZEN)
    (nft-transfer? stacksmint-nft-v2-1-3 token-id sender recipient)))

;; Bulk transfer
(define-public (bulk-transfer 
  (transfers (list 25 { token-id: uint, recipient: principal })))
  (fold bulk-transfer-iter transfers (ok u0)))

(define-private (bulk-transfer-iter 
  (item { token-id: uint, recipient: principal }) 
  (prev (response uint uint)))
  (match prev
    success (match (transfer (get token-id item) tx-sender (get recipient item))
      result (ok (+ success u1))
      err prev)
    err prev))

;; ============================================================================
;; Approval Functions
;; ============================================================================

(define-public (approve (spender principal) (token-id uint))
  (let ((owner (unwrap! (nft-get-owner? stacksmint-nft-v2-1-3 token-id) ERR_NOT_FOUND)))
    (asserts! (is-eq tx-sender owner) ERR_NOT_OWNER)
    (map-set token-approvals token-id spender)
    (ok true)))

(define-public (set-approval-for-all (operator principal) (approved bool))
  (begin
    (map-set operator-approvals { owner: tx-sender, operator: operator } approved)
    (ok true)))

;; ============================================================================
;; Burn Function
;; ============================================================================

(define-public (burn (token-id uint))
  (begin
    (asserts! (is-owner-or-approved token-id tx-sender) ERR_NOT_AUTHORIZED)
    (try! (nft-burn? stacksmint-nft-v2-1-3 token-id tx-sender))
    (var-set total-burned (+ (var-get total-burned) u1))
    (map-delete token-uris token-id)
    (map-delete token-metadata token-id)
    (map-delete frozen-tokens token-id)
    (map-delete token-approvals token-id)
    (ok true)))

;; ============================================================================
;; Freeze Functions
;; ============================================================================

(define-public (freeze-token (token-id uint))
  (begin
    (asserts! (is-owner-or-approved token-id tx-sender) ERR_NOT_AUTHORIZED)
    (map-set frozen-tokens token-id true)
    (ok true)))

(define-public (unfreeze-token (token-id uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (map-delete frozen-tokens token-id)
    (ok true)))

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

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

(define-public (set-placeholder-uri (uri (string-ascii 256)))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (var-set placeholder-uri uri)
    (ok uri)))

;; ============================================================================
;; SIP-009 Required Functions
;; ============================================================================

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

(define-read-only (get-token-uri (token-id uint))
  (ok (map-get? token-uris token-id)))

(define-read-only (get-owner (token-id uint))
  (ok (nft-get-owner? stacksmint-nft-v2-1-3 token-id)))

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

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

(define-read-only (get-total-supply)
  (- (var-get last-token-id) (var-get total-burned)))

(define-read-only (get-total-minted)
  (var-get last-token-id))

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

(define-read-only (is-frozen (token-id uint))
  (is-token-frozen token-id))

(define-read-only (is-revealed (token-id uint))
  (match (map-get? token-metadata token-id)
    metadata (get revealed metadata)
    false))

(define-read-only (get-rarity-score (token-id uint))
  (default-to u0 (map-get? rarity-scores token-id)))

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

(define-read-only (get-delegate-info (owner principal) (delegate principal))
  (map-get? delegates { owner: owner, delegate: delegate }))

(define-read-only (get-reveal-block)
  (var-get reveal-block))

Functions (40)

FunctionAccessArgs
is-owner-or-approvedprivatetoken-id: uint, sender: principal
is-token-frozenprivatetoken-id: uint
is-valid-delegateprivateowner: principal, delegate: principal
can-delegate-mintprivateowner: principal, delegate: principal
mintpublicuri: (string-ascii 256
mint-with-royaltypublicuri: (string-ascii 256
mint-fullpublicuri: (string-ascii 256
batch-mintpublicuris: (list 25 (string-ascii 256
batch-mint-iterprivateuri: (string-ascii 256
add-delegatepublicdelegate: principal, can-mint: bool, can-transfer: bool, can-list: bool, duration: uint
remove-delegatepublicdelegate: principal
mint-as-delegatepublicowner: principal, uri: (string-ascii 256
set-reveal-blockpublicreveal-at: uint
reveal-tokenpublictoken-id: uint
batch-revealpublictoken-ids: (list 25 uint
batch-reveal-iterprivatetoken-id: uint, prev: (response uint uint
calculate-rarity-scorepublictoken-id: uint, score: uint
transferpublictoken-id: uint, sender: principal, recipient: principal
bulk-transferpublictransfers: (list 25 { token-id: uint, recipient: principal }
bulk-transfer-iterprivateitem: { token-id: uint, recipient: principal }, prev: (response uint uint
approvepublicspender: principal, token-id: uint
set-approval-for-allpublicoperator: principal, approved: bool
burnpublictoken-id: uint
freeze-tokenpublictoken-id: uint
unfreeze-tokenpublictoken-id: uint
set-minting-pausedpublicpaused: bool
set-placeholder-uripublicuri: (string-ascii 256
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
get-token-metadataread-onlytoken-id: uint
get-total-supplyread-only
get-total-mintedread-only
get-total-burnedread-only
is-frozenread-onlytoken-id: uint
is-revealedread-onlytoken-id: uint
get-rarity-scoreread-onlytoken-id: uint
get-token-traitsread-onlytoken-id: uint
get-delegate-inforead-onlyowner: principal, delegate: principal
get-reveal-blockread-only