Source Code

;; title: Bitdap Pass
;; version: 0.1.0
;; summary: Bitdap Pass - tiered membership NFT collection on Stacks.
;; description: >
;;   Bitdap Pass is a non-fungible token (NFT) collection that represents
;;   access passes to the Bitdap ecosystem. Each pass belongs to a tier
;;   (Basic, Pro, or VIP), which can be used by off-chain services or
;;   other contracts to gate features and experiences.
;;
;;   - Collection name: Bitdap Pass
;;   - Tiers: Basic, Pro, VIP
;;   - 1 owner per token-id, non-fractional NFTs
;;   - Future milestones will define minting, transfer logic, and metadata
;;     for each tier.
;;
;;   - mint-event: emitted when a pass is minted (token-id, owner, tier)
;;   - transfer-event: emitted when ownership changes (token-id, from, to)
;;   - burn-event: emitted when a pass is burned (token-id, owner, tier)

;; traits
;; - Trait definitions can be added here (e.g., SIP-009) for interface compatibility.

;; token definitions
;; - Bitdap Pass uses uint token-ids (u1, u2, ...) to identify each NFT.
;; - Each token-id is associated with exactly one owner and one tier.
;;

;; constants
;; - Collection-wide configuration, tier identifiers, and error codes.

;; Error codes
(define-constant ERR-INVALID-TIER (err u100))
(define-constant ERR-NOT-FOUND (err u101))
(define-constant ERR-NOT-OWNER (err u102))
(define-constant ERR-SELF-TRANSFER (err u103))
(define-constant ERR-MAX-SUPPLY (err u104))
(define-constant ERR-MAX-TIER-SUPPLY (err u105))

;; Tiers are represented as uints for compact on-chain storage.
(define-constant TIER-BASIC u1)
(define-constant TIER-PRO u2)
(define-constant TIER-VIP u3)

;; Collection-wide and per-tier maximum supplies.
;; These are conservative defaults that can be adjusted in future versions.
(define-constant MAX-SUPPLY u10000)
(define-constant MAX-BASIC-SUPPLY u7000)
(define-constant MAX-PRO-SUPPLY u2500)
(define-constant MAX-VIP-SUPPLY u500)

;; data vars
;; - Global counters for token-ids and total supply.

;; Next token-id to mint (starts at u1).
(define-data-var next-token-id uint u1)

;; Total number of Bitdap Pass NFTs currently in circulation.
(define-data-var total-supply uint u0)

;; data maps
;; - Storage for token ownership, metadata, and per-tier supply.

;; token-id -> owner principal
(define-map token-owners
    { token-id: uint }
    { owner: principal }
)

;; token-id -> metadata (tier and optional off-chain URI)
(define-map token-metadata
    { token-id: uint }
    {
        tier: uint,
        uri: (optional (string-utf8 256)),
    }
)

;; tier -> current supply for that tier
(define-map tier-supplies
    { tier: uint }
    { supply: uint }
)

;; public functions
;; - Core NFT operations: mint, transfer, burn.

;; Mint a new Bitdap Pass NFT for the caller in the given tier.
;; - The recipient is always tx-sender for now; this keeps permissions simple:
;;   users can only mint passes for themselves.
;; - Enforces global and per-tier maximum supplies.
;; - Returns (ok token-id) on success, or an error constant on failure.
(define-read-only (get-max-supply)
    MAX-SUPPLY
)

(define-public (mint-pass
        (tier uint)
        (uri (optional (string-utf8 256)))
    )
    (begin
        ;; Validate tier first.
        (if (not (is-valid-tier tier))
            ERR-INVALID-TIER
            (let (
                    (current-total (var-get total-supply))
                    (new-total (+ current-total u1))
                )
                ;; Check global max supply.
                (if (> new-total MAX-SUPPLY)
                    ERR-MAX-SUPPLY
                    (let (
                            (tier-row (default-to { supply: u0 } (map-get? tier-supplies { tier: tier })))
                            (tier-supply (get supply tier-row))
                            (new-tier-supply (+ tier-supply u1))
                        )
                        ;; Check per-tier max supply.
                        (if (is-tier-over-max? tier new-tier-supply)
                            ERR-MAX-TIER-SUPPLY
                            (let (
                                    (token-id (var-get next-token-id))
                                    (recipient tx-sender)
                                )
                                (begin
                                    ;; Write ownership and metadata.
                                    (map-set token-owners { token-id: token-id } { owner: recipient })
                                    (map-set token-metadata { token-id: token-id } {
                                        tier: tier,
                                        uri: uri,
                                    })
                                    ;; Update counters.
                                    (map-set tier-supplies { tier: tier } { supply: new-tier-supply })
                                    (var-set total-supply new-total)
                                    (var-set next-token-id (+ token-id u1))
                                    ;; Emit mint event.
                                    (print (tuple
                                        (event "mint-event")
                                        (token-id token-id)
                                        (owner recipient)
                                        (tier tier)
                                    ))
                                    (ok token-id)
                                )
                            )
                        )
                    )
                )
            )
        )
    )
)

;; Transfer a Bitdap Pass NFT from the caller to a recipient.
;; - Only the current owner may transfer.
;; - Self-transfers (owner == recipient) are rejected as no-ops.
(define-public (transfer
        (token-id uint)
        (recipient principal)
    )
    (let ((owner-row (map-get? token-owners { token-id: token-id })))
        (match owner-row
            owner-data (let ((owner (get owner owner-data)))
                (if (not (is-eq owner tx-sender))
                    ERR-NOT-OWNER
                    (if (is-eq owner recipient)
                        ERR-SELF-TRANSFER
                        (begin
                            (map-set token-owners { token-id: token-id } { owner: recipient })
                            ;; Emit transfer event.
                            (print (tuple
                                (event "transfer-event")
                                (token-id token-id)
                                (from owner)
                                (to recipient)
                            ))
                            (ok true)
                        )
                    )
                )
            )
            ERR-NOT-FOUND
        )
    )
)

;; Burn (destroy) a Bitdap Pass NFT owned by the caller.
;; - Only the current owner may burn.
;; - Decrements total supply and the tier supply.
(define-public (burn (token-id uint))
    (let ((owner-row (map-get? token-owners { token-id: token-id })))
        (match owner-row
            owner-data (let ((owner (get owner owner-data)))
                (if (not (is-eq owner tx-sender))
                    ERR-NOT-OWNER
                    (let ((meta-row (map-get? token-metadata { token-id: token-id })))
                        (match meta-row
                            meta (let (
                                    (tier (get tier meta))
                                    (tier-row (default-to { supply: u0 } (map-get? tier-supplies { tier: tier })))
                                    (current-tier-supply (get supply tier-row))
                                )
                                (begin
                                    ;; Delete ownership and metadata.
                                    (map-delete token-owners { token-id: token-id })
                                    (map-delete token-metadata { token-id: token-id })
                                    ;; Decrement counters (clamped to zero by design choices).
                                    (map-set tier-supplies { tier: tier } { supply: (if (> current-tier-supply u0)
                                        (- current-tier-supply u1)
                                        u0
                                    ) }
                                    )
                                    (let ((current-total (var-get total-supply)))
                                        (var-set total-supply
                                            (if (> current-total u0)
                                                (- current-total u1)
                                                u0
                                            ))
                                    )
                                    ;; Emit burn event.
                                    (print (tuple
                                        (event "burn-event")
                                        (token-id token-id)
                                        (owner owner)
                                        (tier tier)
                                    ))
                                    (ok true)
                                )
                            )
                            ERR-NOT-FOUND
                        )
                    )
                )
            )
            ERR-NOT-FOUND
        )
    )
)

;; read only functions
;; - Views into ownership, metadata, and supply statistics (SIP-009 style).

;; SIP-009: returns the last minted token-id (or u0 if none exist).
(define-read-only (get-last-token-id)
    (let ((next-id (var-get next-token-id)))
        (if (is-eq next-id u1)
            (ok u0)
            (ok (- next-id u1))
        )
    )
)

;; SIP-009: returns the owner of a given token-id, if any.
(define-read-only (get-owner (token-id uint))
    (match (map-get? token-owners { token-id: token-id })
        owner-row (ok (get owner owner-row))
        ERR-NOT-FOUND
    )
)

;; SIP-009: returns the token URI for a given token-id, if present.
(define-read-only (get-token-uri (token-id uint))
    (match (map-get? token-metadata { token-id: token-id })
        meta (ok (get uri meta))
        ERR-NOT-FOUND
    )
)

;; Returns the full metadata record for a given token-id.
(define-read-only (get-token-metadata (token-id uint))
    (match (map-get? token-metadata { token-id: token-id })
        meta (ok meta)
        ERR-NOT-FOUND
    )
)

;; Returns the tier (Basic/Pro/VIP) for a given token-id.
(define-read-only (get-tier (token-id uint))
    (match (map-get? token-metadata { token-id: token-id })
        meta (ok (get tier meta))
        ERR-NOT-FOUND
    )
)

;; Returns the total supply of Bitdap Pass NFTs in circulation.
(define-read-only (get-total-supply)
    (ok (var-get total-supply))
)

;; Returns the supply for a specific tier.
(define-read-only (get-tier-supply (tier uint))
    (let (
            (row (default-to { supply: u0 } (map-get? tier-supplies { tier: tier })))
        )
        (ok (get supply row))
    )
)

;; private functions
;; - Internal helpers for validating tiers and managing counters/maps.

;; Returns true if the given tier is one of the known tiers.
(define-private (is-valid-tier (tier uint))
    (or
        (is-eq tier TIER-BASIC)
        (or
            (is-eq tier TIER-PRO)
            (is-eq tier TIER-VIP)
        )
    )
)

;; Returns true if the given (tier, new-supply) would exceed that tier's max.
(define-private (is-tier-over-max?
        (tier uint)
        (new-supply uint)
    )
    (if (is-eq tier TIER-BASIC)
        (> new-supply MAX-BASIC-SUPPLY)
        (if (is-eq tier TIER-PRO)
            (> new-supply MAX-PRO-SUPPLY)
            (if (is-eq tier TIER-VIP)
                (> new-supply MAX-VIP-SUPPLY)
                ;; Unknown tiers are treated as invalid via is-valid-tier, but
                ;; this path is left as not over max for safety.
                false
            )
        )
    )
)

Functions (11)

FunctionAccessArgs
get-max-supplyread-only
mint-passpublictier: uint, uri: (optional (string-utf8 256
burnpublictoken-id: uint
get-last-token-idread-only
get-ownerread-onlytoken-id: uint
get-token-uriread-onlytoken-id: uint
get-token-metadataread-onlytoken-id: uint
get-tierread-onlytoken-id: uint
get-total-supplyread-only
get-tier-supplyread-onlytier: uint
is-valid-tierprivatetier: uint