Source Code

;; quest-badge-nft
;; Quest Badge NFT - Minted upon quest completion following SIP-009 NFT standard
;; Each badge represents mastery of a specific DeFi protocol

;; Implement SIP-009 NFT trait
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; Define the NFT
(define-non-fungible-token quest-badge uint)

;; Data vars
(define-data-var last-token-id uint u0)
(define-data-var base-token-uri (string-ascii 256) "https://stxfinance.xyz/api/metadata/")

;; Constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_OWNER_ONLY (err u100))
(define-constant ERR_NOT_TOKEN_OWNER (err u101))
(define-constant ERR_UNAUTHORIZED (err u103))
(define-constant ERR_ALREADY_CLAIMED (err u104))
(define-constant ERR_INVALID_QUEST (err u105))

;; Map token ID to quest info
(define-map token-info
    uint
    {
        protocol: (string-ascii 50),
        owner: principal,
        completion-date: uint,
        xp-earned: uint,
    }
)

;; Map user + protocol to token ID (prevent duplicate claims)
(define-map user-protocol-badge
    {
        user: principal,
        protocol: (string-ascii 50),
    }
    uint
)

;; Track total badges minted per protocol
(define-map protocol-badge-count
    (string-ascii 50)
    uint
)

;; Valid protocol names
(define-map valid-protocols
    (string-ascii 50)
    {
        active: bool,
        xp-reward: uint,
    }
)

;; 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))
    (if (> (len (var-get base-token-uri)) u0)
        (ok (some (var-get base-token-uri)))
        (ok none)
    )
)

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

;; Transfer function - Soul-bound badges cannot be transferred
;; They represent personal achievement and cannot be traded
(define-public (transfer
        (token-id uint)
        (sender principal)
        (recipient principal)
    )
    ERR_UNAUTHORIZED
)

;; Core minting function

;; Mint NFT badge for quest completion
;; Anyone can call this to mint their own badge after completing a quest
(define-public (mint-badge (protocol (string-ascii 50)))
    (let (
            (token-id (+ (var-get last-token-id) u1))
            (protocol-info (unwrap! (map-get? valid-protocols protocol) ERR_INVALID_QUEST))
            (current-count (default-to u0 (map-get? protocol-badge-count protocol)))
        )
        ;; Check if protocol is active
        (asserts! (get active protocol-info) ERR_INVALID_QUEST)

        ;; Check if user already has badge for this protocol
        (asserts!
            (is-none (map-get? user-protocol-badge {
                user: tx-sender,
                protocol: protocol,
            }))
            ERR_ALREADY_CLAIMED
        )

        ;; Mint the NFT to the caller
        (try! (nft-mint? quest-badge token-id tx-sender))

        ;; Update data
        (var-set last-token-id token-id)
        (map-set token-info token-id {
            protocol: protocol,
            owner: tx-sender,
            completion-date: stacks-block-height,
            xp-earned: (get xp-reward protocol-info),
        })
        (map-set user-protocol-badge {
            user: tx-sender,
            protocol: protocol,
        }
            token-id
        )
        (map-set protocol-badge-count protocol (+ current-count u1))

        (ok token-id)
    )
)

;; Read-only functions

;; Get badge information
(define-read-only (get-badge-info (token-id uint))
    (ok (map-get? token-info token-id))
)

;; Get user's badge for a specific protocol
(define-read-only (get-user-badge
        (user principal)
        (protocol (string-ascii 50))
    )
    (ok (map-get? user-protocol-badge {
        user: user,
        protocol: protocol,
    }))
)

;; Check if user has completed a protocol quest
(define-read-only (has-completed-protocol
        (user principal)
        (protocol (string-ascii 50))
    )
    (ok (is-some (map-get? user-protocol-badge {
        user: user,
        protocol: protocol,
    })))
)

;; Get total badges minted for a protocol
(define-read-only (get-protocol-badge-count (protocol (string-ascii 50)))
    (ok (default-to u0 (map-get? protocol-badge-count protocol)))
)

;; Get protocol info
(define-read-only (get-protocol-info (protocol (string-ascii 50)))
    (ok (map-get? valid-protocols protocol))
)

;; Admin functions

;; Initialize or update a protocol
;; #[allow(unchecked_data)]
(define-public (set-protocol
        (protocol (string-ascii 50))
        (active bool)
        (xp-reward uint)
    )
    (begin
        (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)
        (map-set valid-protocols protocol {
            active: active,
            xp-reward: xp-reward,
        })
        (ok true)
    )
)

;; Set base token URI
;; #[allow(unchecked_data)]
(define-public (set-base-token-uri (uri (string-ascii 256)))
    (begin
        (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)
        (var-set base-token-uri uri)
        (ok true)
    )
)

;; Initialize default protocols 
(map-set valid-protocols "zest" {
    active: true,
    xp-reward: u50,
})
(map-set valid-protocols "stackingdao" {
    active: true,
    xp-reward: u60,
})
(map-set valid-protocols "granite" {
    active: true,
    xp-reward: u70,
})
(map-set valid-protocols "hermetica" {
    active: true,
    xp-reward: u65,
})
(map-set valid-protocols "arkadiko" {
    active: true,
    xp-reward: u55,
})

Functions (11)

FunctionAccessArgs
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
mint-badgepublicprotocol: (string-ascii 50
get-badge-inforead-onlytoken-id: uint
get-user-badgeread-onlyuser: principal, protocol: (string-ascii 50
has-completed-protocolread-onlyuser: principal, protocol: (string-ascii 50
get-protocol-badge-countread-onlyprotocol: (string-ascii 50
get-protocol-inforead-onlyprotocol: (string-ascii 50
set-protocolpublicprotocol: (string-ascii 50
set-base-token-uripublicuri: (string-ascii 256