Source Code

;; PopPredict Achievement NFTs
;; SIP-009 compliant NFT contract for user achievements
;; Version: 1.0.0

;; ============================================
;; TRAITS
;; ============================================

;; Note: Implement SIP-009 trait in production with correct trait reference
;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; ============================================
;; CONSTANTS
;; ============================================

;; Error codes
(define-constant ERR-NOT-AUTHORIZED (err u200))
(define-constant ERR-NOT-FOUND (err u201))
(define-constant ERR-ALREADY-EXISTS (err u202))
(define-constant ERR-INVALID-ACHIEVEMENT (err u203))
(define-constant ERR-ACHIEVEMENT-LOCKED (err u204))

;; Contract owner
(define-constant CONTRACT-OWNER tx-sender)

;; Achievement types
(define-constant ACHIEVEMENT-FIRST-PREDICTION u1)
(define-constant ACHIEVEMENT-FIRST-WIN u2)
(define-constant ACHIEVEMENT-FIVE-WINS u3)
(define-constant ACHIEVEMENT-TEN-WINS u4)
(define-constant ACHIEVEMENT-HUNDRED-STX-EARNED u5)
(define-constant ACHIEVEMENT-PERFECT-WEEK u6)
(define-constant ACHIEVEMENT-EARLY-ADOPTER u7)
(define-constant ACHIEVEMENT-WHALE u8)
(define-constant ACHIEVEMENT-CONSISTENT-TRADER u9)
(define-constant ACHIEVEMENT-CATEGORY-MASTER u10)

;; ============================================
;; DATA VARIABLES
;; ============================================

(define-data-var token-id-nonce uint u0)
(define-data-var poppredict-contract principal CONTRACT-OWNER)

;; ============================================
;; DATA MAPS
;; ============================================

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

;; Track which achievements users have earned
(define-map user-achievements
  { user: principal, achievement-type: uint }
  { token-id: uint, earned-at: uint }
)

;; Achievement metadata
(define-map achievement-metadata
  { achievement-type: uint }
  {
    name: (string-ascii 50),
    description: (string-utf8 256),
    image-uri: (string-ascii 256),
    enabled: bool
  }
)

;; User achievement counters
(define-map user-achievement-stats
  { user: principal }
  {
    total-predictions: uint,
    total-wins: uint,
    total-stx-earned: uint,
    achievement-count: uint
  }
)

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

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

(define-private (is-poppredict-contract)
  (is-eq contract-caller (var-get poppredict-contract))
)

(define-private (get-user-stats-or-default (user principal))
  (default-to
    { total-predictions: u0, total-wins: u0, total-stx-earned: u0, achievement-count: u0 }
    (map-get? user-achievement-stats { user: user })
  )
)

;; ============================================
;; SIP-009 IMPLEMENTATION
;; ============================================

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

(define-read-only (get-token-uri (token-id uint))
  (match (map-get? token-owners { token-id: token-id })
    owner-data
      (let
        (
          (achievement-type (unwrap! (get-achievement-type-by-token token-id) ERR-NOT-FOUND))
          (metadata (unwrap! (map-get? achievement-metadata { achievement-type: achievement-type }) ERR-NOT-FOUND))
        )
        (ok (some (get image-uri metadata)))
      )
    ERR-NOT-FOUND
  )
)

(define-read-only (get-owner (token-id uint))
  (match (map-get? token-owners { token-id: token-id })
    owner-data (ok (some (get owner owner-data)))
    (ok none)
  )
)

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
  ;; Achievement NFTs are soulbound - cannot be transferred
  ERR-ACHIEVEMENT-LOCKED
)

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

(define-read-only (get-user-achievement (user principal) (achievement-type uint))
  (map-get? user-achievements { user: user, achievement-type: achievement-type })
)

(define-read-only (has-achievement (user principal) (achievement-type uint))
  (is-some (get-user-achievement user achievement-type))
)

(define-read-only (get-achievement-metadata-info (achievement-type uint))
  (map-get? achievement-metadata { achievement-type: achievement-type })
)

(define-read-only (get-user-stats-info (user principal))
  (ok (get-user-stats-or-default user))
)

;; Clarity 4: Get contract verification hash
(define-read-only (get-nft-contract-verification)
  (ok {
    contract-hash: (contract-hash? .achievement-nft),
    current-time: stacks-block-time
  })
)

;; Alternative verification info
(define-read-only (get-nft-contract-info)
  (ok {
    current-time: stacks-block-time,
    total-tokens: (var-get token-id-nonce)
  })
)

(define-read-only (get-achievement-type-by-token (token-id uint))
  (let
    (
      (owner-data (unwrap! (map-get? token-owners { token-id: token-id }) ERR-NOT-FOUND))
      (owner (get owner owner-data))
    )
    ;; Search through user achievements to find matching token-id
    ;; This is a simplified version - in production, consider additional tracking
    (ok ACHIEVEMENT-FIRST-PREDICTION) ;; Placeholder - would need reverse lookup map
  )
)

;; ============================================
;; PUBLIC FUNCTIONS - ADMIN
;; ============================================

(define-public (set-poppredict-contract (new-contract principal))
  (begin
    (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
    (ok (var-set poppredict-contract new-contract))
  )
)

(define-public (set-achievement-metadata
  (achievement-type uint)
  (name (string-ascii 50))
  (description (string-utf8 256))
  (image-uri (string-ascii 256))
  (enabled bool)
)
  (begin
    (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
    (ok (map-set achievement-metadata
      { achievement-type: achievement-type }
      {
        name: name,
        description: description,
        image-uri: image-uri,
        enabled: enabled
      }
    ))
  )
)

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

(define-public (mint-achievement (user principal) (achievement-type uint))
  (let
    (
      (new-token-id (var-get token-id-nonce))
      (existing-achievement (get-user-achievement user achievement-type))
      (metadata (unwrap! (map-get? achievement-metadata { achievement-type: achievement-type }) ERR-INVALID-ACHIEVEMENT))
      (user-stats (get-user-stats-or-default user))
    )
    (asserts! (or (is-contract-owner) (is-poppredict-contract)) ERR-NOT-AUTHORIZED)
    (asserts! (get enabled metadata) ERR-INVALID-ACHIEVEMENT)
    (asserts! (is-none existing-achievement) ERR-ALREADY-EXISTS)
    
    ;; Mint NFT
    (map-set token-owners
      { token-id: new-token-id }
      { owner: user }
    )
    
    ;; Record achievement
    (map-set user-achievements
      { user: user, achievement-type: achievement-type }
      { token-id: new-token-id, earned-at: stacks-block-height }
    )
    
    ;; Update user stats
    (map-set user-achievement-stats
      { user: user }
      (merge user-stats { achievement-count: (+ (get achievement-count user-stats) u1) })
    )
    
    ;; Increment token ID
    (var-set token-id-nonce (+ new-token-id u1))
    
    ;; Clarity 4: Log achievement mint event (using burn-block-height for Clarity 3 compatibility)
    (print {
      event: "achievement-minted",
      user: user,
      achievement-type: achievement-type,
      token-id: new-token-id,
      burn-block: burn-block-height,  ;; Clarity 4: Replace with stacks-block-time
      block-height: stacks-block-height
    })
    
    (ok new-token-id)
  )
)

;; ============================================
;; PUBLIC FUNCTIONS - STAT TRACKING
;; ============================================

(define-public (increment-predictions (user principal))
  (let
    (
      (stats (get-user-stats-or-default user))
      (new-total (+ (get total-predictions stats) u1))
    )
    (asserts! (or (is-contract-owner) (is-poppredict-contract)) ERR-NOT-AUTHORIZED)
    
    (map-set user-achievement-stats
      { user: user }
      (merge stats { total-predictions: new-total })
    )
    
    ;; Clarity 4: Log prediction increment (using burn-block-height for Clarity 3 compatibility)
    (print {
      event: "prediction-tracked",
      user: user,
      total-predictions: new-total,
      burn-block: burn-block-height  ;; Clarity 4: Replace with stacks-block-time
    })
    
    ;; Auto-mint first prediction achievement
    (if (is-eq new-total u1)
      (mint-achievement user ACHIEVEMENT-FIRST-PREDICTION)
      (ok u0)
    )
  )
)

(define-public (increment-wins (user principal))
  (let
    (
      (stats (get-user-stats-or-default user))
      (new-total (+ (get total-wins stats) u1))
    )
    (asserts! (or (is-contract-owner) (is-poppredict-contract)) ERR-NOT-AUTHORIZED)
    
    (map-set user-achievement-stats
      { user: user }
      (merge stats { total-wins: new-total })
    )
    
    ;; Clarity 4: Log win increment (using burn-block-height for Clarity 3 compatibility)
    (print {
      event: "win-tracked",
      user: user,
      total-wins: new-total,
      burn-block: burn-block-height  ;; Clarity 4: Replace with stacks-block-time
    })
    
    ;; Auto-mint win achievements
    (if (is-eq new-total u1)
      (mint-achievement user ACHIEVEMENT-FIRST-WIN)
      (if (is-eq new-total u5)
        (mint-achievement user ACHIEVEMENT-FIVE-WINS)
        (if (is-eq new-total u10)
          (mint-achievement user ACHIEVEMENT-TEN-WINS)
          (ok u0)
        )
      )
    )
  )
)

(define-public (add-stx-earned (user principal) (amount uint))
  (let
    (
      (stats (get-user-stats-or-default user))
      (new-total (+ (get total-stx-earned stats) amount))
    )
    (asserts! (or (is-contract-owner) (is-poppredict-contract)) ERR-NOT-AUTHORIZED)
    
    (map-set user-achievement-stats
      { user: user }
      (merge stats { total-stx-earned: new-total })
    )
    
    ;; Clarity 4: Log STX earned increment
    (print {
      event: "stx-earned-tracked",
      user: user,
      amount: amount,
      total-earned: new-total,
      timestamp: stacks-block-time
    })
    
    ;; Auto-mint STX earned achievement (100 STX = 100,000,000 microSTX)
    (if (>= new-total u100000000)
      (mint-achievement user ACHIEVEMENT-HUNDRED-STX-EARNED)
      (ok u0)
    )
  )
)

;; ============================================
;; INITIALIZATION
;; ============================================

;; Initialize default achievement metadata
(map-set achievement-metadata
  { achievement-type: ACHIEVEMENT-FIRST-PREDICTION }
  {
    name: "First Prediction",
    description: u"Made your first prediction on PopPredict",
    image-uri: "ipfs://placeholder/first-prediction.png",
    enabled: true
  }
)

(map-set achievement-metadata
  { achievement-type: ACHIEVEMENT-FIRST-WIN }
  {
    name: "First Win",
    description: u"Won your first prediction market",
    image-uri: "ipfs://placeholder/first-win.png",
    enabled: true
  }
)

(map-set achievement-metadata
  { achievement-type: ACHIEVEMENT-FIVE-WINS }
  {
    name: "Rising Star",
    description: u"Won 5 prediction markets",
    image-uri: "ipfs://placeholder/five-wins.png",
    enabled: true
  }
)

(map-set achievement-metadata
  { achievement-type: ACHIEVEMENT-TEN-WINS }
  {
    name: "Prophet",
    description: u"Won 10 prediction markets",
    image-uri: "ipfs://placeholder/ten-wins.png",
    enabled: true
  }
)

(map-set achievement-metadata
  { achievement-type: ACHIEVEMENT-HUNDRED-STX-EARNED }
  {
    name: "Century Club",
    description: u"Earned 100 STX in total winnings",
    image-uri: "ipfs://placeholder/hundred-stx.png",
    enabled: true
  }
)

Functions (20)

FunctionAccessArgs
is-contract-ownerprivate
is-poppredict-contractprivate
get-user-stats-or-defaultprivateuser: principal
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
transferpublictoken-id: uint, sender: principal, recipient: principal
get-user-achievementread-onlyuser: principal, achievement-type: uint
has-achievementread-onlyuser: principal, achievement-type: uint
get-achievement-metadata-inforead-onlyachievement-type: uint
get-user-stats-inforead-onlyuser: principal
get-nft-contract-verificationread-only
get-nft-contract-inforead-only
get-achievement-type-by-tokenread-onlytoken-id: uint
set-poppredict-contractpublicnew-contract: principal
set-achievement-metadatapublicachievement-type: uint, name: (string-ascii 50
mint-achievementpublicuser: principal, achievement-type: uint
increment-predictionspublicuser: principal
increment-winspublicuser: principal
add-stx-earnedpublicuser: principal, amount: uint