Source Code

;; Skill Certification NFT Contract (Clarity 4)
;; SIP-009 compliant NFT for verified skill certifications
;; Uses stacks-block-time for expiration tracking

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

;; token definitions
(define-non-fungible-token skill-certification uint)

;; constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_UNAUTHORIZED (err u2001))
(define-constant ERR_NOT_FOUND (err u2002))
(define-constant ERR_ALREADY_CERTIFIED (err u2003))
(define-constant ERR_EXPIRED (err u2004))
(define-constant ERR_INVALID_LEVEL (err u2005))
(define-constant ERR_NOT_OWNER (err u2006))

(define-constant LEVEL_BRONZE u1)
(define-constant LEVEL_SILVER u2)
(define-constant LEVEL_GOLD u3)
(define-constant LEVEL_PLATINUM u4)

(define-constant CERT_DURATION_BRONZE u15552000)
(define-constant CERT_DURATION_SILVER u31104000)
(define-constant CERT_DURATION_GOLD u62208000)
(define-constant CERT_DURATION_PLATINUM u124416000)

;; data vars
(define-data-var last-token-id uint u0)
(define-data-var total-certifications uint u0)

;; data maps
(define-map certification-data
    uint
    {
        skill-id: uint,
        owner: principal,
        level: uint,
        issued-at: uint,
        expires-at: uint,
        verified-by: principal,
        is-active: bool,
        renewal-count: uint
    })

(define-map user-skill-certs
    { owner: principal, skill-id: uint }
    uint)

(define-map verifiers principal bool)

;; public functions
(define-public (get-last-token-id)
    (ok (var-get last-token-id)))

(define-public (get-token-uri (token-id uint))
    (ok (some "https://timebank.io/certifications/")))

(define-public (get-owner (token-id uint))
    (ok (nft-get-owner? skill-certification token-id)))

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender sender) ERR_UNAUTHORIZED)
        (try! (nft-transfer? skill-certification token-id sender recipient))
        (let ((cert-data (unwrap! (map-get? certification-data token-id) ERR_NOT_FOUND)))
            (map-set certification-data token-id (merge cert-data { owner: recipient })))
        (ok true)))

(define-public (issue-certification
    (recipient principal)
    (skill-id uint)
    (level uint))
    (let ((new-token-id (+ (var-get last-token-id) u1)))
        (asserts! (or (is-eq tx-sender CONTRACT_OWNER) (default-to false (map-get? verifiers tx-sender))) ERR_UNAUTHORIZED)
        (asserts! (and (>= level LEVEL_BRONZE) (<= level LEVEL_PLATINUM)) ERR_INVALID_LEVEL)
        (try! (nft-mint? skill-certification new-token-id recipient))
        (map-set certification-data new-token-id {
            skill-id: skill-id,
            owner: recipient,
            level: level,
            issued-at: stacks-block-time,
            expires-at: (+ stacks-block-time (get-duration-for-level level)),
            verified-by: tx-sender,
            is-active: true,
            renewal-count: u0
        })
        (map-set user-skill-certs { owner: recipient, skill-id: skill-id } new-token-id)
        (var-set last-token-id new-token-id)
        (var-set total-certifications (+ (var-get total-certifications) u1))
        (ok new-token-id)))

(define-public (renew-certification (token-id uint))
    (let ((cert-data (unwrap! (map-get? certification-data token-id) ERR_NOT_FOUND)))
        (asserts! (is-eq tx-sender (get owner cert-data)) ERR_NOT_OWNER)
        (map-set certification-data token-id (merge cert-data {
            expires-at: (+ stacks-block-time (get-duration-for-level (get level cert-data))),
            renewal-count: (+ (get renewal-count cert-data) u1)
        }))
        (ok true)))

(define-public (add-verifier (verifier principal))
    (begin
        (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
        (map-set verifiers verifier true)
        (ok true)))

;; read only functions
(define-read-only (get-certification-info (token-id uint))
    (ok (map-get? certification-data token-id)))

(define-read-only (is-certified (owner principal) (skill-id uint))
    (is-some (get-active-certification owner skill-id)))

(define-read-only (get-active-certification (owner principal) (skill-id uint))
    (match (map-get? user-skill-certs { owner: owner, skill-id: skill-id })
        token-id (let ((cert-data (unwrap-panic (map-get? certification-data token-id))))
            (if (and
                (get is-active cert-data)
                (>= (get expires-at cert-data) stacks-block-time))
                (some cert-data)
                none))
        none))

;; private functions
(define-private (get-duration-for-level (level uint))
    (if (is-eq level LEVEL_BRONZE)
        CERT_DURATION_BRONZE
        (if (is-eq level LEVEL_SILVER)
            CERT_DURATION_SILVER
            (if (is-eq level LEVEL_GOLD)
                CERT_DURATION_GOLD
                CERT_DURATION_PLATINUM))))

Functions (11)

FunctionAccessArgs
get-last-token-idpublic
get-token-uripublictoken-id: uint
get-ownerpublictoken-id: uint
transferpublictoken-id: uint, sender: principal, recipient: principal
issue-certificationpublicrecipient: principal, skill-id: uint, level: uint
renew-certificationpublictoken-id: uint
add-verifierpublicverifier: principal
get-certification-inforead-onlytoken-id: uint
is-certifiedread-onlyowner: principal, skill-id: uint
get-active-certificationread-onlyowner: principal, skill-id: uint
get-duration-for-levelprivatelevel: uint