Source Code

;; NFT Token Contract - SIP-009 Compliant
;; A fully-featured NFT contract with metadata, royalties, and collection management

;; 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-token-exists (err u102))
(define-constant err-token-not-found (err u103))
(define-constant err-invalid-royalty (err u104))
(define-constant err-unauthorized (err u105))
(define-constant err-invalid-metadata (err u106))

;; Data Variables
(define-data-var last-token-id uint u0)
(define-data-var collection-name (string-ascii 64) "SNFT Collection")
(define-data-var collection-symbol (string-ascii 10) "SNFT")
(define-data-var base-uri (string-ascii 256) "ipfs://")
(define-data-var minting-enabled bool true)

;; Data Maps
(define-map token-owners uint principal)
(define-map token-metadata uint {
    uri: (string-ascii 256),
    creator: principal,
    royalty-percent: uint
})
(define-map token-approvals {token-id: uint, spender: principal} bool)
(define-map operator-approvals {owner: principal, operator: principal} bool)

;; NFT Trait Implementation (SIP-009)
(define-non-fungible-token snft-token uint)

;; Read-only 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 (get uri (default-to 
        {uri: "", creator: contract-owner, royalty-percent: u0}
        (map-get? token-metadata token-id)))))
)

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

(define-read-only (get-token-metadata (token-id uint))
    (ok (map-get? token-metadata token-id))
)

(define-read-only (get-token-creator (token-id uint))
    (ok (get creator (default-to 
        {uri: "", creator: contract-owner, royalty-percent: u0}
        (map-get? token-metadata token-id))))
)

(define-read-only (get-token-royalty (token-id uint))
    (ok (get royalty-percent (default-to 
        {uri: "", creator: contract-owner, royalty-percent: u0}
        (map-get? token-metadata token-id))))
)

(define-read-only (get-collection-info)
    (ok {
        name: (var-get collection-name),
        symbol: (var-get collection-symbol),
        base-uri: (var-get base-uri),
        total-supply: (var-get last-token-id)
    })
)

(define-read-only (is-minting-enabled)
    (ok (var-get minting-enabled))
)

(define-read-only (is-approved-spender (token-id uint) (spender principal))
    (ok (default-to false (map-get? token-approvals {token-id: token-id, spender: spender})))
)

(define-read-only (is-approved-operator (owner principal) (operator principal))
    (ok (default-to false (map-get? operator-approvals {owner: owner, operator: operator})))
)

;; Private functions

(define-private (check-token-owner (token-id uint) (sender principal))
    (match (nft-get-owner? snft-token token-id)
        owner (ok (is-eq sender owner))
        (err err-token-not-found)
    )
)

(define-private (is-sender-owner-or-approved (token-id uint) (sender principal))
    (let ((owner (unwrap! (nft-get-owner? snft-token token-id) false)))
        (or 
            (is-eq owner sender)
            (default-to false (map-get? token-approvals {token-id: token-id, spender: sender}))
            (default-to false (map-get? operator-approvals {owner: owner, operator: sender}))
        )
    )
)

;; Public functions

(define-public (mint (recipient principal) (metadata-uri (string-ascii 256)) (royalty-percent uint))
    (let 
        (
            (token-id (+ (var-get last-token-id) u1))
        )
        ;; Validations
        (asserts! (var-get minting-enabled) err-unauthorized)
        (asserts! (> (len metadata-uri) u0) err-invalid-metadata)
        (asserts! (<= royalty-percent u1000) err-invalid-royalty) ;; Max 10% (1000 basis points)
        
        ;; Mint NFT
        (try! (nft-mint? snft-token token-id recipient))
        
        ;; Store metadata
        (map-set token-metadata token-id {
            uri: metadata-uri,
            creator: tx-sender,
            royalty-percent: royalty-percent
        })
        
        ;; Update counter
        (var-set last-token-id token-id)
        
        (print {
            event: "mint",
            token-id: token-id,
            recipient: recipient,
            creator: tx-sender,
            metadata-uri: metadata-uri,
            royalty-percent: royalty-percent
        })
        
        (ok token-id)
    )
)

(define-public (transfer (token-id uint) (sender principal) (recipient principal))
    (begin
        ;; Validations
        (asserts! (is-sender-owner-or-approved token-id tx-sender) err-not-token-owner)
        (asserts! (is-eq sender (unwrap! (nft-get-owner? snft-token token-id) err-token-not-found)) err-not-token-owner)
        
        ;; Clear approvals
        (map-delete token-approvals {token-id: token-id, spender: tx-sender})
        
        ;; Transfer NFT
        (try! (nft-transfer? snft-token token-id sender recipient))
        
        (print {
            event: "transfer",
            token-id: token-id,
            from: sender,
            to: recipient
        })
        
        (ok true)
    )
)

(define-public (approve (token-id uint) (spender principal))
    (let ((owner (unwrap! (nft-get-owner? snft-token token-id) err-token-not-found)))
        (asserts! (is-eq tx-sender owner) err-not-token-owner)
        (map-set token-approvals {token-id: token-id, spender: spender} true)
        
        (print {
            event: "approval",
            token-id: token-id,
            owner: owner,
            spender: spender
        })
        
        (ok true)
    )
)

(define-public (revoke-approval (token-id uint) (spender principal))
    (let ((owner (unwrap! (nft-get-owner? snft-token token-id) err-token-not-found)))
        (asserts! (is-eq tx-sender owner) err-not-token-owner)
        (map-delete token-approvals {token-id: token-id, spender: spender})
        
        (print {
            event: "approval-revoked",
            token-id: token-id,
            owner: owner,
            spender: spender
        })
        
        (ok true)
    )
)

(define-public (set-approved-operator (operator principal) (approved bool))
    (begin
        (if approved
            (map-set operator-approvals {owner: tx-sender, operator: operator} true)
            (map-delete operator-approvals {owner: tx-sender, operator: operator})
        )
        
        (print {
            event: "operator-approval",
            owner: tx-sender,
            operator: operator,
            approved: approved
        })
        
        (ok true)
    )
)

;; Admin functions

(define-public (set-base-uri (new-base-uri (string-ascii 256)))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set base-uri new-base-uri)
        (ok true)
    )
)

(define-public (set-collection-name (new-name (string-ascii 64)))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set collection-name new-name)
        (ok true)
    )
)

(define-public (toggle-minting)
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (var-set minting-enabled (not (var-get minting-enabled)))
        (ok (var-get minting-enabled))
    )
)

(define-public (burn (token-id uint))
    (let ((owner (unwrap! (nft-get-owner? snft-token token-id) err-token-not-found)))
        (asserts! (is-eq tx-sender owner) err-not-token-owner)
        
        ;; Clean up metadata
        (map-delete token-metadata token-id)
        
        ;; Burn NFT
        (try! (nft-burn? snft-token token-id owner))
        
        (print {
            event: "burn",
            token-id: token-id,
            owner: owner
        })
        
        (ok true)
    )
)

Functions (21)

FunctionAccessArgs
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
get-ownerread-onlytoken-id: uint
get-token-metadataread-onlytoken-id: uint
get-token-creatorread-onlytoken-id: uint
get-token-royaltyread-onlytoken-id: uint
get-collection-inforead-only
is-minting-enabledread-only
is-approved-spenderread-onlytoken-id: uint, spender: principal
is-approved-operatorread-onlyowner: principal, operator: principal
check-token-ownerprivatetoken-id: uint, sender: principal
is-sender-owner-or-approvedprivatetoken-id: uint, sender: principal
mintpublicrecipient: principal, metadata-uri: (string-ascii 256
transferpublictoken-id: uint, sender: principal, recipient: principal
approvepublictoken-id: uint, spender: principal
revoke-approvalpublictoken-id: uint, spender: principal
set-approved-operatorpublicoperator: principal, approved: bool
set-base-uripublicnew-base-uri: (string-ascii 256
set-collection-namepublicnew-name: (string-ascii 64
toggle-mintingpublic
burnpublictoken-id: uint