Source Code

;; StackPulse Reputation Badges V3 (SIP-009 NFT)
;; NFT badges for achievements and milestones
;;
;; Badge Types:
;; 1 = Early Adopter (first 100 users)
;; 2 = Whale Watcher (detected 10 whale transfers)
;; 3 = Alert Master (created 25+ alerts)
;; 4 = Power User (Pro or Premium subscriber)
;; 5 = Referral Champion (referred 5+ users)
;; 6 = Year One (subscribed for 1 year)
;; 7 = Community Builder (participated in governance)
;; 8 = Bug Hunter (reported valid bugs)
;; 9 = StackPulse OG (original beta tester)

;; ============================================
;; SIP-009 NFT TRAIT
;; ============================================

(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

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

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-NOT-FOUND (err u101))
(define-constant ERR-ALREADY-MINTED (err u102))
(define-constant ERR-INVALID-BADGE (err u103))

;; Base URI for metadata
(define-constant BASE-URI "https://stackpulse.vercel.app/api/badges/")

;; ============================================
;; NFT DEFINITION
;; ============================================

(define-non-fungible-token stackpulse-badge uint)

;; ============================================
;; DATA STORAGE
;; ============================================

(define-data-var last-token-id uint u0)
(define-data-var total-badges-minted uint u0)

;; Badge metadata
(define-map badge-data uint
  {
    badge-type: uint,
    name: (string-ascii 64),
    recipient: principal,
    minted-at: uint
  }
)

;; Track which badge types a user has
(define-map user-badges { user: principal, badge-type: uint } uint)

;; Badge type definitions
(define-map badge-definitions uint 
  {
    name: (string-ascii 64),
    description: (string-ascii 256),
    max-supply: uint,
    minted-count: uint
  }
)

;; Authorized minters (can be other contracts)
(define-map authorized-minters principal bool)

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

;; Initialize badge definitions
(map-set badge-definitions u1 { name: "Early Adopter", description: "Among the first 100 StackPulse users", max-supply: u100, minted-count: u0 })
(map-set badge-definitions u2 { name: "Whale Watcher", description: "Detected 10+ whale transfers", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u3 { name: "Alert Master", description: "Created 25+ alerts", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u4 { name: "Power User", description: "Pro or Premium subscriber", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u5 { name: "Referral Champion", description: "Referred 5+ users", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u6 { name: "Year One", description: "Active for 1 year", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u7 { name: "Community Builder", description: "Active in governance", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u8 { name: "Bug Hunter", description: "Reported valid bugs", max-supply: u0, minted-count: u0 })
(map-set badge-definitions u9 { name: "StackPulse OG", description: "Original beta tester", max-supply: u50, minted-count: u0 })

;; ============================================
;; 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 (some (concat BASE-URI (uint-to-string token-id))))
)

(define-read-only (get-owner (token-id uint))
  (ok (nft-get-owner? stackpulse-badge token-id))
)

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
  (begin
    (asserts! (is-eq tx-sender sender) ERR-NOT-AUTHORIZED)
    (nft-transfer? stackpulse-badge token-id sender recipient)
  )
)

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

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

(define-read-only (get-badge-definition (badge-type uint))
  (map-get? badge-definitions badge-type)
)

(define-read-only (has-badge (user principal) (badge-type uint))
  (is-some (map-get? user-badges { user: user, badge-type: badge-type }))
)

(define-read-only (get-user-badge-token (user principal) (badge-type uint))
  (map-get? user-badges { user: user, badge-type: badge-type })
)

(define-read-only (get-stats)
  {
    total-minted: (var-get total-badges-minted),
    last-id: (var-get last-token-id)
  }
)

(define-read-only (is-authorized-minter (minter principal))
  (default-to false (map-get? authorized-minters minter))
)

;; ============================================
;; PUBLIC FUNCTIONS
;; ============================================

;; Mint a badge to a user
(define-public (mint-badge (recipient principal) (badge-type uint))
  (let
    (
      (token-id (+ (var-get last-token-id) u1))
      (badge-def (unwrap! (map-get? badge-definitions badge-type) ERR-INVALID-BADGE))
    )
    ;; Check authorization
    (asserts! (or (is-eq tx-sender CONTRACT-OWNER) 
                  (is-authorized-minter tx-sender)) ERR-NOT-AUTHORIZED)
    
    ;; Check if user already has this badge type
    (asserts! (not (has-badge recipient badge-type)) ERR-ALREADY-MINTED)
    
    ;; Check max supply (0 = unlimited)
    (asserts! (or (is-eq (get max-supply badge-def) u0)
                  (< (get minted-count badge-def) (get max-supply badge-def))) 
              ERR-NOT-AUTHORIZED)
    
    ;; Mint NFT
    (try! (nft-mint? stackpulse-badge token-id recipient))
    
    ;; Store badge data
    (map-set badge-data token-id {
      badge-type: badge-type,
      name: (get name badge-def),
      recipient: recipient,
      minted-at: block-height
    })
    
    ;; Track user's badge
    (map-set user-badges { user: recipient, badge-type: badge-type } token-id)
    
    ;; Update badge definition minted count
    (map-set badge-definitions badge-type (merge badge-def {
      minted-count: (+ (get minted-count badge-def) u1)
    }))
    
    ;; Update counters
    (var-set last-token-id token-id)
    (var-set total-badges-minted (+ (var-get total-badges-minted) u1))
    
    (print {
      event: "badge-earned",
      token-id: token-id,
      recipient: recipient,
      badge-type: badge-type,
      badge-name: (get name badge-def),
      block: block-height
    })
    
    (ok token-id)
  )
)

;; Admin: Add authorized minter (for other contracts to mint)
(define-public (add-minter (minter principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (map-set authorized-minters minter true)
    (ok true)
  )
)

;; Admin: Remove authorized minter
(define-public (remove-minter (minter principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (map-delete authorized-minters minter)
    (ok true)
  )
)

;; ============================================
;; UTILITY FUNCTIONS
;; ============================================

;; Helper to convert uint to string (renamed to avoid conflict with built-in)
(define-read-only (uint-to-string (value uint))
  (if (<= value u9)
    (unwrap-panic (element-at "0123456789" value))
    (get r (fold uint-to-string-iter
      (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9)
      { n: value, r: "" }))
  )
)

(define-private (uint-to-string-iter (idx uint) (state { n: uint, r: (string-ascii 10) }))
  (if (> (get n state) u0)
    {
      n: (/ (get n state) u10),
      r: (unwrap-panic (as-max-len? 
        (concat (unwrap-panic (element-at "0123456789" (mod (get n state) u10))) (get r state)) 
        u10))
    }
    state
  )
)

Functions (14)

FunctionAccessArgs
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
transferpublictoken-id: uint, sender: principal, recipient: principal
get-badge-dataread-onlytoken-id: uint
get-badge-definitionread-onlybadge-type: uint
has-badgeread-onlyuser: principal, badge-type: uint
get-user-badge-tokenread-onlyuser: principal, badge-type: uint
get-statsread-only
is-authorized-minterread-onlyminter: principal
mint-badgepublicrecipient: principal, badge-type: uint
add-minterpublicminter: principal
remove-minterpublicminter: principal
uint-to-stringread-onlyvalue: uint