stacksmint-collection-v2-1-3

SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N

Source Code

;; StacksMint Collection Contract v2.1
;; NFT collection management with metadata, royalties, allowlists, and analytics
;; Version: 2.1.0
;; Author: StacksMint Team
;; Upgrade: v2.1 - Allowlists, mint phases, collection analytics, airdrops

;; ============================================================================
;; 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_COLLECTION_EXISTS (err u102))
(define-constant ERR_MAX_SUPPLY_REACHED (err u103))
(define-constant ERR_INVALID_NAME (err u104))
(define-constant ERR_NOT_CREATOR (err u105))
(define-constant ERR_COLLECTION_LOCKED (err u106))
(define-constant ERR_INVALID_ROYALTY (err u107))
(define-constant ERR_MINTING_CLOSED (err u108))
(define-constant ERR_NOT_ALLOWLISTED (err u109))
(define-constant ERR_PHASE_NOT_ACTIVE (err u110))
(define-constant ERR_MINT_LIMIT_REACHED (err u111))
(define-constant ERR_INVALID_PHASE (err u112))
(define-constant ERR_AIRDROP_FAILED (err u113))

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

(define-constant MAX_ROYALTY_PERCENT u100)     ;; 10% max royalty
(define-constant MIN_NAME_LENGTH u3)
(define-constant MAX_DESCRIPTION_LENGTH u500)
(define-constant MAX_AIRDROP_SIZE u50)

;; Mint phases
(define-constant PHASE_CLOSED u0)
(define-constant PHASE_ALLOWLIST u1)
(define-constant PHASE_PUBLIC u2)

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

(define-data-var collection-counter uint u0)
(define-data-var total-minted-all uint u0)

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

;; Collection core data
(define-map collections uint {
  name: (string-ascii 64),
  creator: principal,
  max-supply: uint,
  minted-count: uint,
  royalty-percent: uint,
  created-at: uint,
  is-locked: bool,
  minting-open: bool,
  mint-price: uint,
  current-phase: uint
})

;; Collection metadata
(define-map collection-metadata uint {
  description: (string-ascii 500),
  image-uri: (string-ascii 256),
  banner-uri: (string-ascii 256),
  website: (string-ascii 128),
  twitter: (string-ascii 64),
  discord: (string-ascii 64)
})

;; Verified collections (by platform admin)
(define-map verified-collections uint bool)

;; Collection statistics
(define-map collection-stats uint {
  floor-price: uint,
  total-volume: uint,
  total-sales: uint,
  unique-owners: uint,
  listed-count: uint,
  average-price: uint
})

;; Track collection ownership
(define-map collection-ownership { collection-id: uint, owner: principal } uint)

;; Allowlist for mint phases
(define-map allowlists { collection-id: uint, user: principal } {
  spots: uint,
  minted: uint,
  phase: uint
})

;; Mint limits per wallet
(define-map mint-limits { collection-id: uint, wallet: principal } {
  total-minted: uint,
  allowlist-minted: uint,
  public-minted: uint
})

;; Collection mint phases
(define-map mint-phases { collection-id: uint, phase: uint } {
  start-block: uint,
  end-block: uint,
  price: uint,
  max-per-wallet: uint
})

;; Airdrop recipients
(define-map airdrop-recipients { collection-id: uint, recipient: principal } uint)

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

(define-private (is-collection-creator (collection-id uint))
  (match (map-get? collections collection-id)
    collection (is-eq tx-sender (get creator collection))
    false))

(define-private (is-phase-active (collection-id uint) (phase uint))
  (match (map-get? mint-phases { collection-id: collection-id, phase: phase })
    phase-data (and 
      (>= block-height (get start-block phase-data))
      (< block-height (get end-block phase-data)))
    false))

(define-private (is-on-allowlist (collection-id uint) (user principal))
  (match (map-get? allowlists { collection-id: collection-id, user: user })
    entry (> (get spots entry) (get minted entry))
    false))

;; ============================================================================
;; Collection Management Functions
;; ============================================================================

;; Create new collection
(define-public (create-collection (name (string-ascii 64)) (max-supply uint))
  (create-collection-full name max-supply u0 u1000000 "" "" "" "" "" ""))

;; Create collection with full metadata
(define-public (create-collection-full 
  (name (string-ascii 64))
  (max-supply uint)
  (royalty-percent uint)
  (mint-price uint)
  (description (string-ascii 500))
  (image-uri (string-ascii 256))
  (banner-uri (string-ascii 256))
  (website (string-ascii 128))
  (twitter (string-ascii 64))
  (discord (string-ascii 64)))
  (begin
    (asserts! (>= (len name) MIN_NAME_LENGTH) ERR_INVALID_NAME)
    (asserts! (<= royalty-percent MAX_ROYALTY_PERCENT) ERR_INVALID_ROYALTY)
    (let ((collection-id (+ (var-get collection-counter) u1)))
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      (map-set collections collection-id {
        name: name,
        creator: tx-sender,
        max-supply: max-supply,
        minted-count: u0,
        royalty-percent: royalty-percent,
        created-at: block-height,
        is-locked: false,
        minting-open: false,
        mint-price: mint-price,
        current-phase: PHASE_CLOSED
      })
      (map-set collection-metadata collection-id {
        description: description,
        image-uri: image-uri,
        banner-uri: banner-uri,
        website: website,
        twitter: twitter,
        discord: discord
      })
      (map-set collection-stats collection-id {
        floor-price: u0,
        total-volume: u0,
        total-sales: u0,
        unique-owners: u0,
        listed-count: u0,
        average-price: u0
      })
      (var-set collection-counter collection-id)
      (ok collection-id))))

;; ============================================================================
;; Mint Phase Functions
;; ============================================================================

;; Set mint phase
(define-public (set-mint-phase 
  (collection-id uint) 
  (phase uint) 
  (start-block uint) 
  (end-block uint) 
  (price uint)
  (max-per-wallet uint))
  (begin
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (asserts! (and (>= phase PHASE_CLOSED) (<= phase PHASE_PUBLIC)) ERR_INVALID_PHASE)
    (map-set mint-phases { collection-id: collection-id, phase: phase } {
      start-block: start-block,
      end-block: end-block,
      price: price,
      max-per-wallet: max-per-wallet
    })
    (ok true)))

;; Activate phase
(define-public (activate-phase (collection-id uint) (phase uint))
  (let ((collection (unwrap! (map-get? collections collection-id) ERR_NOT_FOUND)))
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-set collections collection-id (merge collection { 
      current-phase: phase,
      minting-open: (> phase PHASE_CLOSED)
    }))
    (ok phase)))

;; ============================================================================
;; Allowlist Functions
;; ============================================================================

;; Add to allowlist (single)
(define-public (add-to-allowlist (collection-id uint) (user principal) (spots uint))
  (begin
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-set allowlists { collection-id: collection-id, user: user } {
      spots: spots,
      minted: u0,
      phase: PHASE_ALLOWLIST
    })
    (ok true)))

;; Add multiple to allowlist
(define-public (batch-add-allowlist 
  (collection-id uint) 
  (users (list 50 { user: principal, spots: uint })))
  (begin
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (fold add-allowlist-iter users (ok collection-id))))

(define-private (add-allowlist-iter 
  (entry { user: principal, spots: uint }) 
  (prev (response uint uint)))
  (match prev
    collection-id (begin
      (map-set allowlists { collection-id: collection-id, user: (get user entry) } {
        spots: (get spots entry),
        minted: u0,
        phase: PHASE_ALLOWLIST
      })
      (ok collection-id))
    err prev))

;; Remove from allowlist
(define-public (remove-from-allowlist (collection-id uint) (user principal))
  (begin
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-delete allowlists { collection-id: collection-id, user: user })
    (ok true)))

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

;; Mint from collection (respects phases)
(define-public (mint-from-collection (collection-id uint))
  (let ((collection (unwrap! (map-get? collections collection-id) ERR_NOT_FOUND))
        (current-phase (get current-phase collection)))
    (asserts! (get minting-open collection) ERR_MINTING_CLOSED)
    (asserts! (not (get is-locked collection)) ERR_COLLECTION_LOCKED)
    (asserts! (< (get minted-count collection) (get max-supply collection)) ERR_MAX_SUPPLY_REACHED)
    
    ;; Check phase requirements
    (if (is-eq current-phase PHASE_ALLOWLIST)
      (begin
        (asserts! (is-on-allowlist collection-id tx-sender) ERR_NOT_ALLOWLISTED)
        (mint-allowlist collection-id collection))
      (if (is-eq current-phase PHASE_PUBLIC)
        (mint-public collection-id collection)
        ERR_PHASE_NOT_ACTIVE))))

;; Allowlist mint logic
(define-private (mint-allowlist (collection-id uint) (collection {
  name: (string-ascii 64),
  creator: principal,
  max-supply: uint,
  minted-count: uint,
  royalty-percent: uint,
  created-at: uint,
  is-locked: bool,
  minting-open: bool,
  mint-price: uint,
  current-phase: uint
}))
  (let ((allowlist-entry (unwrap! (map-get? allowlists { collection-id: collection-id, user: tx-sender }) ERR_NOT_ALLOWLISTED))
        (phase-data (map-get? mint-phases { collection-id: collection-id, phase: PHASE_ALLOWLIST })))
    (asserts! (< (get minted allowlist-entry) (get spots allowlist-entry)) ERR_MINT_LIMIT_REACHED)
    
    ;; Get mint price
    (let ((mint-price (match phase-data data (get price data) (get mint-price collection))))
      ;; Pay mint price
      (try! (stx-transfer? mint-price tx-sender (get creator collection)))
      ;; Collect platform fee
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      
      ;; Update allowlist entry
      (map-set allowlists { collection-id: collection-id, user: tx-sender }
        (merge allowlist-entry { minted: (+ (get minted allowlist-entry) u1) }))
      
      ;; Update collection stats
      (map-set collections collection-id 
        (merge collection { minted-count: (+ (get minted-count collection) u1) }))
      (var-set total-minted-all (+ (var-get total-minted-all) u1))
      
      ;; Track mint
      (update-mint-tracking collection-id tx-sender true)
      
      (ok (get minted-count collection)))))

;; Public mint logic
(define-private (mint-public (collection-id uint) (collection {
  name: (string-ascii 64),
  creator: principal,
  max-supply: uint,
  minted-count: uint,
  royalty-percent: uint,
  created-at: uint,
  is-locked: bool,
  minting-open: bool,
  mint-price: uint,
  current-phase: uint
}))
  (let ((phase-data (map-get? mint-phases { collection-id: collection-id, phase: PHASE_PUBLIC }))
        (limits (default-to { total-minted: u0, allowlist-minted: u0, public-minted: u0 }
          (map-get? mint-limits { collection-id: collection-id, wallet: tx-sender }))))
    
    ;; Check wallet limit
    (match phase-data 
      data (asserts! (< (get public-minted limits) (get max-per-wallet data)) ERR_MINT_LIMIT_REACHED)
      true)
    
    ;; Get mint price
    (let ((mint-price (match phase-data data (get price data) (get mint-price collection))))
      ;; Pay mint price
      (try! (stx-transfer? mint-price tx-sender (get creator collection)))
      ;; Collect platform fee
      (try! (contract-call? 'SP3FKNEZ86RG5RT7SZ5FBRGH85FZNG94ZH1MCGG6N.stacksmint-treasury-v2-1 collect-fee))
      
      ;; Update collection stats
      (map-set collections collection-id 
        (merge collection { minted-count: (+ (get minted-count collection) u1) }))
      (var-set total-minted-all (+ (var-get total-minted-all) u1))
      
      ;; Track mint
      (update-mint-tracking collection-id tx-sender false)
      
      (ok (get minted-count collection)))))

;; Update mint tracking
(define-private (update-mint-tracking (collection-id uint) (wallet principal) (is-allowlist bool))
  (let ((current (default-to { total-minted: u0, allowlist-minted: u0, public-minted: u0 }
          (map-get? mint-limits { collection-id: collection-id, wallet: wallet }))))
    (map-set mint-limits { collection-id: collection-id, wallet: wallet } {
      total-minted: (+ (get total-minted current) u1),
      allowlist-minted: (if is-allowlist (+ (get allowlist-minted current) u1) (get allowlist-minted current)),
      public-minted: (if is-allowlist (get public-minted current) (+ (get public-minted current) u1))
    })
    true))

;; ============================================================================
;; Airdrop Functions
;; ============================================================================

;; Airdrop to multiple recipients
(define-public (airdrop 
  (collection-id uint) 
  (recipients (list 50 principal)))
  (let ((collection (unwrap! (map-get? collections collection-id) ERR_NOT_FOUND)))
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (asserts! (<= (len recipients) MAX_AIRDROP_SIZE) ERR_AIRDROP_FAILED)
    (asserts! (<= (+ (get minted-count collection) (len recipients)) (get max-supply collection)) ERR_MAX_SUPPLY_REACHED)
    
    ;; Process airdrops
    (fold airdrop-iter recipients (ok { collection-id: collection-id, count: u0 }))))

(define-private (airdrop-iter 
  (recipient principal) 
  (prev (response { collection-id: uint, count: uint } uint)))
  (match prev
    data (let ((collection (unwrap! (map-get? collections (get collection-id data)) ERR_NOT_FOUND)))
      ;; Update collection
      (map-set collections (get collection-id data)
        (merge collection { minted-count: (+ (get minted-count collection) u1) }))
      ;; Track airdrop
      (map-set airdrop-recipients { collection-id: (get collection-id data), recipient: recipient }
        (+ (default-to u0 (map-get? airdrop-recipients { collection-id: (get collection-id data), recipient: recipient })) u1))
      (var-set total-minted-all (+ (var-get total-minted-all) u1))
      (ok { collection-id: (get collection-id data), count: (+ (get count data) u1) }))
    err prev))

;; ============================================================================
;; Collection Update Functions
;; ============================================================================

;; Update metadata
(define-public (update-collection-metadata
  (collection-id uint)
  (description (string-ascii 500))
  (image-uri (string-ascii 256))
  (banner-uri (string-ascii 256))
  (website (string-ascii 128))
  (twitter (string-ascii 64))
  (discord (string-ascii 64)))
  (begin
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-set collection-metadata collection-id {
      description: description,
      image-uri: image-uri,
      banner-uri: banner-uri,
      website: website,
      twitter: twitter,
      discord: discord
    })
    (ok true)))

;; Set mint price
(define-public (set-mint-price (collection-id uint) (price uint))
  (let ((collection (unwrap! (map-get? collections collection-id) ERR_NOT_FOUND)))
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-set collections collection-id (merge collection { mint-price: price }))
    (ok price)))

;; Lock collection (permanent)
(define-public (lock-collection (collection-id uint))
  (let ((collection (unwrap! (map-get? collections collection-id) ERR_NOT_FOUND)))
    (asserts! (is-collection-creator collection-id) ERR_NOT_CREATOR)
    (map-set collections collection-id (merge collection { is-locked: true }))
    (ok true)))

;; ============================================================================
;; Stats Update Functions (called by marketplace)
;; ============================================================================

(define-public (update-collection-stats 
  (collection-id uint)
  (floor-price uint)
  (sale-volume uint))
  (let ((stats (default-to {
    floor-price: u0,
    total-volume: u0,
    total-sales: u0,
    unique-owners: u0,
    listed-count: u0,
    average-price: u0
  } (map-get? collection-stats collection-id))))
    (let ((new-volume (+ (get total-volume stats) sale-volume))
          (new-sales (+ (get total-sales stats) u1)))
      (map-set collection-stats collection-id (merge stats {
        floor-price: floor-price,
        total-volume: new-volume,
        total-sales: new-sales,
        average-price: (/ new-volume new-sales)
      }))
      (ok true))))

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

;; Verify collection (platform admin only)
(define-public (verify-collection (collection-id uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (map-set verified-collections collection-id true)
    (ok true)))

;; Unverify collection
(define-public (unverify-collection (collection-id uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_OWNER)
    (map-delete verified-collections collection-id)
    (ok true)))

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

(define-read-only (get-collection (collection-id uint))
  (map-get? collections collection-id))

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

(define-read-only (get-collection-stats (collection-id uint))
  (map-get? collection-stats collection-id))

(define-read-only (is-verified (collection-id uint))
  (default-to false (map-get? verified-collections collection-id)))

(define-read-only (get-allowlist-status (collection-id uint) (user principal))
  (map-get? allowlists { collection-id: collection-id, user: user }))

(define-read-only (get-mint-limits (collection-id uint) (wallet principal))
  (map-get? mint-limits { collection-id: collection-id, wallet: wallet }))

(define-read-only (get-mint-phase (collection-id uint) (phase uint))
  (map-get? mint-phases { collection-id: collection-id, phase: phase }))

(define-read-only (get-total-collections)
  (var-get collection-counter))

(define-read-only (get-total-minted)
  (var-get total-minted-all))

(define-read-only (get-collection-full (collection-id uint))
  {
    collection: (map-get? collections collection-id),
    metadata: (map-get? collection-metadata collection-id),
    stats: (map-get? collection-stats collection-id),
    is-verified: (default-to false (map-get? verified-collections collection-id))
  })

Functions (31)

FunctionAccessArgs
get-total-collectionsread-only
is-collection-creatorprivatecollection-id: uint
is-phase-activeprivatecollection-id: uint, phase: uint
is-on-allowlistprivatecollection-id: uint, user: principal
create-collectionpublicname: (string-ascii 64
create-collection-fullpublicname: (string-ascii 64
set-mint-phasepubliccollection-id: uint, phase: uint, start-block: uint, end-block: uint, price: uint, max-per-wallet: uint
activate-phasepubliccollection-id: uint, phase: uint
add-to-allowlistpubliccollection-id: uint, user: principal, spots: uint
batch-add-allowlistpubliccollection-id: uint, users: (list 50 { user: principal, spots: uint }
add-allowlist-iterprivateentry: { user: principal, spots: uint }, prev: (response uint uint
remove-from-allowlistpubliccollection-id: uint, user: principal
mint-from-collectionpubliccollection-id: uint
update-mint-trackingprivatecollection-id: uint, wallet: principal, is-allowlist: bool
airdroppubliccollection-id: uint, recipients: (list 50 principal
airdrop-iterprivaterecipient: principal, prev: (response { collection-id: uint, count: uint } uint
update-collection-metadatapubliccollection-id: uint, description: (string-ascii 500
set-mint-pricepubliccollection-id: uint, price: uint
lock-collectionpubliccollection-id: uint
update-collection-statspubliccollection-id: uint, floor-price: uint, sale-volume: uint
verify-collectionpubliccollection-id: uint
unverify-collectionpubliccollection-id: uint
get-collectionread-onlycollection-id: uint
get-collection-metadataread-onlycollection-id: uint
get-collection-statsread-onlycollection-id: uint
is-verifiedread-onlycollection-id: uint
get-allowlist-statusread-onlycollection-id: uint, user: principal
get-mint-limitsread-onlycollection-id: uint, wallet: principal
get-mint-phaseread-onlycollection-id: uint, phase: uint
get-total-mintedread-only
get-collection-fullread-onlycollection-id: uint