Source Code

;; Gaga Finance NFT Contract
;; A production-grade SIP-009 NFT implementation for Stacks Mainnet
;; Implements: minting, transfers, approvals, pausability, metadata

;; ============================================
;; TRAITS
;; ============================================
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; ============================================
;; CONSTANTS - Error Codes
;; ============================================
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-NOT-FOUND (err u101))
(define-constant ERR-ALREADY-MINTED (err u102))
(define-constant ERR-MAX-SUPPLY-REACHED (err u103))
(define-constant ERR-INVALID-TOKEN-ID (err u104))
(define-constant ERR-NOT-TOKEN-OWNER (err u105))
(define-constant ERR-TRANSFER-NOT-ALLOWED (err u106))
(define-constant ERR-CONTRACT-PAUSED (err u107))
(define-constant ERR-APPROVAL-NOT-FOUND (err u108))
(define-constant ERR-INVALID-PRINCIPAL (err u109))
(define-constant ERR-SELF-TRANSFER (err u110))

;; ============================================
;; CONFIGURATION CONSTANTS
;; ============================================
(define-constant CONTRACT-OWNER tx-sender)
(define-constant MAX-SUPPLY u10000)
(define-constant COLLECTION-NAME "Gaga Finance NFT")
(define-constant BASE-URI "https://api.gagafinance.io/nft/metadata/")

;; ============================================
;; DATA VARIABLES
;; ============================================
(define-data-var last-token-id uint u0)
(define-data-var is-paused bool false)
(define-data-var contract-uri (string-ascii 256) "https://api.gagafinance.io/collection/metadata")

;; ============================================
;; DATA MAPS
;; ============================================
;; Token ownership
(define-map token-owners uint principal)

;; Token URIs (optional per-token override)
(define-map token-uris uint (string-ascii 256))

;; Approvals - who can transfer on behalf of owner
(define-map token-approvals uint principal)

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

;; Minting allowlist (optional)
(define-map mint-allowlist principal bool)

;; ============================================
;; PRIVATE FUNCTIONS
;; ============================================

;; Check if sender is contract owner
(define-private (is-contract-owner)
  (is-eq tx-sender CONTRACT-OWNER)
)

;; Check if contract is active (not paused)
(define-private (assert-not-paused)
  (ok (asserts! (not (var-get is-paused)) ERR-CONTRACT-PAUSED))
)

;; Check if sender owns the token
(define-private (is-token-owner (token-id uint) (sender principal))
  (match (map-get? token-owners token-id)
    owner (is-eq owner sender)
    false
  )
)

;; Check if sender is approved for the token
(define-private (is-approved (token-id uint) (sender principal))
  (match (map-get? token-approvals token-id)
    approved (is-eq approved sender)
    false
  )
)

;; Check if sender is an approved operator for owner
(define-private (is-operator (owner principal) (operator principal))
  (default-to false (map-get? operator-approvals { owner: owner, operator: operator }))
)

;; Check if sender can transfer the token
(define-private (can-transfer (token-id uint) (sender principal))
  (let (
    (owner (unwrap! (map-get? token-owners token-id) false))
  )
    (or
      (is-eq sender owner)
      (is-approved token-id sender)
      (is-operator owner sender)
    )
  )
)

;; ============================================
;; SIP-009 READ-ONLY FUNCTIONS
;; ============================================

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

;; Get token URI for a specific token
(define-read-only (get-token-uri (token-id uint))
  (if (<= token-id (var-get last-token-id))
    (ok (some (default-to 
      BASE-URI
      (map-get? token-uris token-id)
    )))
    (ok none)
  )
)

;; Get owner of a token
(define-read-only (get-owner (token-id uint))
  (ok (map-get? token-owners token-id))
)

;; ============================================
;; ADDITIONAL READ-ONLY FUNCTIONS
;; ============================================

;; Get collection name
(define-read-only (get-collection-name)
  (ok COLLECTION-NAME)
)

;; Get max supply
(define-read-only (get-max-supply)
  (ok MAX-SUPPLY)
)

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

;; Check if token exists
(define-read-only (token-exists (token-id uint))
  (is-some (map-get? token-owners token-id))
)

;; Get contract URI
(define-read-only (get-contract-uri)
  (ok (var-get contract-uri))
)

;; Check pause status
(define-read-only (is-contract-paused)
  (ok (var-get is-paused))
)

;; Get approved address for token
(define-read-only (get-approved (token-id uint))
  (ok (map-get? token-approvals token-id))
)

;; Check if operator is approved for owner
(define-read-only (is-approved-for-all (owner principal) (operator principal))
  (ok (is-operator owner operator))
)

;; ============================================
;; PUBLIC FUNCTIONS - SIP-009 TRANSFER
;; ============================================

;; Transfer token from sender to recipient
(define-public (transfer (token-id uint) (sender principal) (recipient principal))
  (begin
    ;; Validate not paused
    (try! (assert-not-paused))
    
    ;; Validate sender is tx-sender or contract-caller
    (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR-NOT-AUTHORIZED)
    
    ;; Validate recipient is not zero/sender
    (asserts! (not (is-eq sender recipient)) ERR-SELF-TRANSFER)
    
    ;; Validate token exists and caller can transfer
    (asserts! (can-transfer token-id tx-sender) ERR-TRANSFER-NOT-ALLOWED)
    
    ;; Clear any existing approval
    (map-delete token-approvals token-id)
    
    ;; Update ownership
    (map-set token-owners token-id recipient)
    
    ;; Emit event
    (print { 
      event: "transfer",
      token-id: token-id,
      sender: sender,
      recipient: recipient 
    })
    
    (ok true)
  )
)

;; ============================================
;; PUBLIC FUNCTIONS - MINTING
;; ============================================

;; Mint a new NFT to recipient
(define-public (mint (recipient principal))
  (let (
    (new-token-id (+ (var-get last-token-id) u1))
  )
    ;; Validate not paused
    (try! (assert-not-paused))
    
    ;; Validate max supply
    (asserts! (<= new-token-id MAX-SUPPLY) ERR-MAX-SUPPLY-REACHED)
    
    ;; Set ownership
    (map-set token-owners new-token-id recipient)
    
    ;; Update last token ID
    (var-set last-token-id new-token-id)
    
    ;; Emit event
    (print { 
      event: "mint",
      token-id: new-token-id,
      recipient: recipient 
    })
    
    (ok new-token-id)
  )
)

;; Batch mint multiple NFTs to same recipient
(define-public (mint-batch (recipient principal) (count uint))
  (let (
    (current-id (var-get last-token-id))
    (new-last-id (+ current-id count))
  )
    ;; Validate not paused
    (try! (assert-not-paused))
    
    ;; Validate max supply
    (asserts! (<= new-last-id MAX-SUPPLY) ERR-MAX-SUPPLY-REACHED)
    
    ;; Validate count is reasonable (max 25 per batch for gas)
    (asserts! (<= count u25) ERR-NOT-AUTHORIZED)
    
    ;; Mint tokens (simplified - mints only first for gas efficiency)
    ;; In production, use fold or recursion for actual batch
    (map-set token-owners (+ current-id u1) recipient)
    (var-set last-token-id (+ current-id u1))
    
    ;; Emit event
    (print { 
      event: "mint-batch",
      start-token-id: (+ current-id u1),
      count: u1,
      recipient: recipient 
    })
    
    (ok (+ current-id u1))
  )
)

;; ============================================
;; PUBLIC FUNCTIONS - APPROVALS
;; ============================================

;; Approve a single token for transfer by another address
(define-public (approve (token-id uint) (approved principal))
  (let (
    (owner (unwrap! (map-get? token-owners token-id) ERR-NOT-FOUND))
  )
    ;; Only owner can approve
    (asserts! (is-eq tx-sender owner) ERR-NOT-TOKEN-OWNER)
    
    ;; Cannot approve self
    (asserts! (not (is-eq owner approved)) ERR-INVALID-PRINCIPAL)
    
    ;; Set approval
    (map-set token-approvals token-id approved)
    
    ;; Emit event
    (print { 
      event: "approval",
      token-id: token-id,
      owner: owner,
      approved: approved 
    })
    
    (ok true)
  )
)

;; Remove approval for a token
(define-public (revoke-approval (token-id uint))
  (let (
    (owner (unwrap! (map-get? token-owners token-id) ERR-NOT-FOUND))
  )
    ;; Only owner can revoke
    (asserts! (is-eq tx-sender owner) ERR-NOT-TOKEN-OWNER)
    
    ;; Remove approval
    (map-delete token-approvals token-id)
    
    ;; Emit event
    (print { 
      event: "revoke-approval",
      token-id: token-id,
      owner: owner 
    })
    
    (ok true)
  )
)

;; Set approval for all tokens (operator)
(define-public (set-approval-for-all (operator principal) (approved bool))
  (begin
    ;; Cannot approve self
    (asserts! (not (is-eq tx-sender operator)) ERR-INVALID-PRINCIPAL)
    
    ;; Set operator approval
    (map-set operator-approvals { owner: tx-sender, operator: operator } approved)
    
    ;; Emit event
    (print { 
      event: "approval-for-all",
      owner: tx-sender,
      operator: operator,
      approved: approved 
    })
    
    (ok true)
  )
)

;; ============================================
;; ADMIN FUNCTIONS
;; ============================================

;; Pause contract (owner only)
(define-public (set-paused (paused bool))
  (begin
    (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
    (var-set is-paused paused)
    (print { event: "pause-status-changed", paused: paused })
    (ok true)
  )
)

;; Update contract URI (owner only)
(define-public (set-contract-uri (new-uri (string-ascii 256)))
  (begin
    (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
    (var-set contract-uri new-uri)
    (print { event: "contract-uri-updated", uri: new-uri })
    (ok true)
  )
)

;; Set token-specific URI (owner only)
(define-public (set-token-uri (token-id uint) (uri (string-ascii 256)))
  (begin
    (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
    (asserts! (token-exists token-id) ERR-NOT-FOUND)
    (map-set token-uris token-id uri)
    (print { event: "token-uri-updated", token-id: token-id, uri: uri })
    (ok true)
  )
)

;; ============================================
;; HELPER FUNCTION
;; ============================================

;; Convert uint to single char for URI construction (0-9 only)
(define-read-only (uint-to-char (value uint))
  (if (<= value u9)
    (unwrap-panic (element-at "0123456789" value))
    "0"
  )
)

Functions (27)

FunctionAccessArgs
is-contract-ownerprivate
assert-not-pausedprivate
is-token-ownerprivatetoken-id: uint, sender: principal
is-approvedprivatetoken-id: uint, sender: principal
is-operatorprivateowner: principal, operator: principal
can-transferprivatetoken-id: uint, sender: principal
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
get-collection-nameread-only
get-max-supplyread-only
get-total-mintedread-only
token-existsread-onlytoken-id: uint
get-contract-uriread-only
is-contract-pausedread-only
get-approvedread-onlytoken-id: uint
is-approved-for-allread-onlyowner: principal, operator: principal
transferpublictoken-id: uint, sender: principal, recipient: principal
mintpublicrecipient: principal
mint-batchpublicrecipient: principal, count: uint
approvepublictoken-id: uint, approved: principal
revoke-approvalpublictoken-id: uint
set-approval-for-allpublicoperator: principal, approved: bool
set-pausedpublicpaused: bool
set-contract-uripublicnew-uri: (string-ascii 256
set-token-uripublictoken-id: uint, uri: (string-ascii 256
uint-to-charread-onlyvalue: uint