Source Code

;; title: gated.so 
;; version: 0.006
;; summary: Page management for Gated
;; description: Smart contract to create and manage pages on gated.so

;; trait for SIP 09 Compliance
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

;; define non-fungible-token
(define-non-fungible-token gated-page uint)

;;Define Constant
;; Error codes
(define-constant ERR_PAGE_EXISTS (err u100))
(define-constant ERR_INVALID_PAGE (err u101))
(define-constant ERR_TITLE_INVALID_LENGTH (err u102))
(define-constant ERR_DESC_INVALID_LENGTH (err u103))
(define-constant ERR_UNAUTHORIZED (err u104))
(define-constant ERR_NOT_TOKEN_OWNER (err u105))
(define-constant ERR_INVALID_TOKEN (err u106))
(define-constant ERR_DUPLICATE_TITLE (err u107))
(define-constant ERR-INVALID-PERCENTAGE (err u108))
(define-constant ERR-INVALID-USER (err u109))
(define-constant ERR-NOT-AUTHORIZED (err u110))
(define-constant ERR-WRONG-COMMISSION (err u111))
(define-constant ERR-NOT-FOUND (err u112))
(define-constant ERR-LISTING (err u113))
(define-constant ERR-TRANSFER (err u114))
(define-constant ERR-INSUFFICIENT-BALANCE (err u115))

;; Min and max length constraints
(define-constant MIN_TITLE_LENGTH u1)
(define-constant MAX_TITLE_LENGTH u64)
(define-constant MIN_DESC_LENGTH u1)
(define-constant MAX_DESC_LENGTH u256)

;; hardcoded gated address
(define-constant gated 'SP2BRB6P0BK6T35DHTGXCV6MZ5TGRN5E0RKZ1T8B5)

;; set the deployer
(define-constant deployer tx-sender)

;; Data variables ;;
;; last-token-id variable
(define-data-var last-token-id uint u0)
;; gated-fee set to 1 STX
(define-data-var gated-fee uint u1000000)

;; Add these required read-only functions
(define-read-only (get-last-token-id)
    (ok (var-get last-token-id)))

;; URI for metadata associated with the token
(define-read-only (get-token-uri (token-id uint))
    (let 
        ((token-data (map-get? token-metadata token-id)))
        (match token-data
        data (ok (some (get uri-suffix data)))
        (ok none))))

;; NFT transfer function
(define-public (transfer (id uint) (sender principal) (recipient principal))
    (if (and 
        (is-eq tx-sender sender)
        (is-none (map-get? market id)))
        (nft-transfer? gated-page id sender recipient)
        (err u1)
    )
)

;; get owner function
(define-read-only (get-owner (id uint))
    (ok (nft-get-owner? gated-page id))
)

;; URI for metadata associated with the token
(define-read-only (get-creator (id uint))
   (map-get? page-creators id))


;; maps for id
    (define-map pages-by-id 
        { id: uint}
        {owner: principal,  
        title: (string-ascii 64), 
        description: (string-ascii 256),
        active: bool}
    )

;; maps for title
    (define-map pages-by-title
        { owner: principal, title: (string-ascii 64) }
        { id: uint }
    )

;; page-creators for royalty
    (define-map page-creators
    uint  ;; token ID
    principal  ;; creator address
)

    ;; Map to store token-specific URI suffixes
(define-map token-metadata
    uint
    {
        uri-suffix: (string-ascii 256),
        version: uint  ;; For tracking metadata updates
    }
)

;; Private functions
;;validate length of title
(define-private (validate-string-length-title (str (string-ascii 64)) (min uint) (max uint))
    (let
    ((str-length (len str)))  ;; Changed from 'len' to 'str-length')
    (and (>= str-length min) (<= str-length max)))
)
;;validate length of description
(define-private (validate-string-length-description (str (string-ascii 256)) (min uint) (max uint))
    (let
    ((str-length (len str)) )
    (and (>= str-length min) (<= str-length max)))
)

;; Add a new page
(define-public (mint-page (title (string-ascii 64)) (description (string-ascii 256)) (metadata-uri (string-ascii 256))) 
    (let
        ((new-id (+ (var-get last-token-id) u1)))
    (begin
        ;; Input validation
        (asserts! (validate-string-length-title title MIN_TITLE_LENGTH MAX_TITLE_LENGTH) ERR_TITLE_INVALID_LENGTH)
        (asserts! (validate-string-length-description description MIN_DESC_LENGTH MAX_DESC_LENGTH) ERR_DESC_INVALID_LENGTH)

        ;; Check if title already exists for this owner
        (asserts! (is-none (map-get? pages-by-title { owner: tx-sender, title: title })) ERR_DUPLICATE_TITLE)

        ;; Assigns page-creator for royalty purposes
        (map-set page-creators new-id tx-sender) 
        
        ;; Process payment for page
        (try! (stx-transfer? (var-get gated-fee) tx-sender gated))

        ;; mint NFT 
        (try! (nft-mint? gated-page new-id tx-sender))  

        ;; Add the page to map
        (map-set pages-by-id 
            { id: new-id }
                {
                owner: tx-sender, 
                title: title,
                description: description,
                active: true
                }
        )
        
        ;; Store in reverse lookup map
        (map-set pages-by-title
            { owner: tx-sender, title: title }
            { id: new-id }
        )

        ;; store token-metadata
        (map-set token-metadata
                    new-id
                    {
                        uri-suffix: metadata-uri,  ;; Use the provided IPFS hash
                        version: (+ (default-to u0 (get version (map-get? token-metadata new-id))) u1)
                    }
                )
        
        ;; Update the page counter
        (var-set last-token-id new-id)

        ;; Emit event
        (print {event: "page-created", page-id: new-id, owner: tx-sender,  metadata-uri: metadata-uri})
        
        ;; Return the new page ID
        (ok new-id)
        )
    )
)

;; is-sender-owner?
(define-private (is-sender-owner (token-id uint))
    (match (nft-get-owner? gated-page token-id)
        owner (is-eq tx-sender owner)
        false
    )
)

;; update-page
(define-public (update-page (id uint) (title (string-ascii 64)) (description (string-ascii 256)) (active bool) (metadata-uri (string-ascii 256)))
    (begin
        (asserts! (is-sender-owner id) ERR-NOT-AUTHORIZED)
        ;; Input validation
        (asserts! (validate-string-length-title title MIN_TITLE_LENGTH MAX_TITLE_LENGTH) ERR_TITLE_INVALID_LENGTH)
        (asserts! (validate-string-length-description description MIN_DESC_LENGTH MAX_DESC_LENGTH) ERR_DESC_INVALID_LENGTH)

            ;; Process payment for page
            (try! (stx-transfer? (var-get gated-fee) tx-sender gated))

        ;; Check if page exists and get it
        (match (map-get? pages-by-id { id: id })
            existing-page-data  ;; More descriptive name
            (begin
                ;; Verify the stored owner matches the sender
                (asserts! (is-eq (get owner existing-page-data) tx-sender) ERR_UNAUTHORIZED)

            ;; If title is different from current, check it's not already used
                (if (not (is-eq title (get title existing-page-data)))
                    (asserts! (is-none (map-get? pages-by-title { owner: tx-sender, title: title })) ERR_DUPLICATE_TITLE)
                    true
                )

                ;; Update the page
                (some (map-set pages-by-id
                    { id: id }
                    {
                    owner: tx-sender,
                    title: title,
                    description: description,
                    active: active
                    }
                ))

                ;; Delete old title mapping if title has changed
                (if (not (is-eq title (get title existing-page-data)))
                    (map-delete pages-by-title { owner: tx-sender, title: (get title existing-page-data) })
                    true
                )

                ;; Update the reverse lookup map
                (map-set pages-by-title
                    { owner: tx-sender, title: title }
                    { id: id }
                )

                ;; Update token metadata with the provided IPFS hash
                (map-set token-metadata
                    id
                    {
                        uri-suffix: metadata-uri,  ;; Use the provided IPFS hash
                        version: (+ (default-to u0 (get version (map-get? token-metadata id))) u1)
                    }
                )

                ;; Emit event
                (print {event: "page successfully updated", owner: tx-sender, active: active, title: title, description: description, metadata-uri: metadata-uri})

                ;; Return success
                (ok true)
            )
            ERR_INVALID_PAGE
        )
    )
)

;; Contract owner functions
(define-public (update-fee (new-fee uint))
    (begin
        (asserts! (is-eq tx-sender deployer) ERR_UNAUTHORIZED)
        (var-set gated-fee new-fee)
        (print {event: "fee-updated", new-fee: new-fee})
        (ok true)
    )
)

;; Look up page by ID
(define-read-only (get-page-by-id (page-id uint))
    (map-get? pages-by-id { id: page-id })
)

;; Read page details
(define-read-only (get-page-by-title (owner principal) (title (string-ascii 64)))
    (match (map-get? pages-by-title { owner: owner, title: title })
        id-data 
        (match (map-get? pages-by-id { id: (get id id-data) })
            page-data (some (merge page-data { id: (get id id-data) }))
            none
        )
        none
    )
)

;; trait for managing commissions as necessary by decentralized marketplaces
(use-trait commission-trait 'SP3D6PV2ACBPEKYJTCMH7HEN02KP87QSP8KTEH335.commission-trait.commission)

(define-map token-count principal uint)
(define-map market uint {price: uint, commission: principal, royalty: uint})

(define-read-only (get-balance (account principal))
    (default-to u0
    (map-get? token-count account))
)

(define-private (trnsfr (token-id uint) (sender principal) (recipient principal))
    (begin
        ;; Check if the sender owns the NFT
        (asserts! (is-eq (unwrap! (nft-get-owner? gated-page token-id) ERR-NOT-FOUND) sender) (err u112))
        
        ;; Perform the NFT transfer
        (try! (nft-transfer? gated-page token-id sender recipient))
        
        (ok true)
    )
)

(define-read-only (get-listing-in-ustx (id uint))
    (map-get? market id)
)

(define-public (list-in-ustx (id uint) (price uint) (comm-trait <commission-trait>))
        (let ((listing  {price: price, commission: (contract-of comm-trait), royalty: (var-get royalty-percent)}))
        
        (asserts! (is-sender-owner id) (err ERR-NOT-AUTHORIZED))
        (map-set market id listing)
        (print (merge listing {a: "list-in-ustx", id: id}))
    (ok true))
)

(define-public (unlist-in-ustx (id uint))
    (begin
    (asserts! (is-sender-owner id) (err ERR-NOT-AUTHORIZED))
    (map-delete market id)
    (print {a: "unlist-in-ustx", id: id})
    (ok true))
)

(define-public (buy-in-ustx (id uint) (comm-trait <commission-trait>))
    (let 
    ((owner (unwrap! (nft-get-owner? gated-page id) (err u1)))
    (listing (unwrap! (map-get? market id) (err u2)))
    (price (get price listing))
    (royalty (get royalty listing)))
    
    ;; First check commission contract
    (if (not (is-eq (contract-of comm-trait) (get commission listing)))
        (err u3)
        (begin
            ;; Combine all operations that need to succeed
            (try! (stx-transfer? price tx-sender owner))
            (try! (pay-royalty price royalty id))
            (try! (contract-call? comm-trait pay id price))
            (try! (trnsfr id owner tx-sender))
            (map-delete market id)
            (print {a: "buy-in-ustx", id: id})
            (ok true))))
)

(define-data-var royalty-percent uint u500)

(define-read-only (get-royalty-percent)
    (ok (var-get royalty-percent))
)

(define-public (set-royalty-percent (royalty uint))
    (begin
        (asserts! (is-eq tx-sender deployer) (err ERR-INVALID-USER))
        (asserts! (and (>= royalty u0) (<= royalty u1000)) (err ERR-INVALID-PERCENTAGE))
        (ok (var-set royalty-percent royalty))))


(define-private (pay-royalty (price uint) (royalty uint) (id uint))
    (let (
        (royalty-amount (/ (* price royalty) u10000))
            (creator (unwrap! (map-get? page-creators id) ERR-NOT-FOUND)))
            (if (and (> royalty-amount u0) (not (is-eq tx-sender creator)))
            (try! (stx-transfer? royalty-amount tx-sender creator))
            (print false))
            (ok true))
)

;; Define a public function that only the deployer can call to airdrop pages to multiple recipients
(define-public (airdrop-pages 
    (recipients (list 200 principal)) 
    (titles (list 200 (string-ascii 64)))
    (descriptions (list 200 (string-ascii 256)))
    (metadata-uri (string-ascii 256)))
    
    (begin
        ;; Check that only the deployer can call this function
        (asserts! (is-eq tx-sender deployer) ERR_UNAUTHORIZED)
        
        ;; Check that all lists have the same length
        (asserts! (is-eq (len recipients) (len titles)) ERR_INVALID_PAGE)
        (asserts! (is-eq (len titles) (len descriptions)) ERR_INVALID_PAGE)
        
        ;; Map through the lists and create pages for each recipient
        (map airdrop-single-page recipients titles descriptions metadata-uri)
        
        (ok true)
    )
)

;; Private function to handle individual page creation during airdrop
(define-private (airdrop-single-page 
    (recipient principal) 
    (title (string-ascii 64)) 
    (description (string-ascii 256))
    (metadata-uri (string-ascii 256)))
    
    (let
        ((new-id (+ (var-get last-token-id) u1)))
        (begin
            ;; Input validation
            (asserts! (validate-string-length-title title MIN_TITLE_LENGTH MAX_TITLE_LENGTH) ERR_TITLE_INVALID_LENGTH)
            (asserts! (validate-string-length-description description MIN_DESC_LENGTH MAX_DESC_LENGTH) ERR_DESC_INVALID_LENGTH)
            
            ;; Check if title already exists for this recipient
            (asserts! (is-none (map-get? pages-by-title { owner: recipient, title: title })) ERR_DUPLICATE_TITLE)

            ;; Mint NFT to recipient
            (try! (nft-mint? gated-page new-id recipient))
            
            ;; Add the page to map
            (map-set pages-by-id 
                { id: new-id }
                {
                    owner: recipient,
                    title: title,
                    description: description,
                    active: true
                }
            )
            
            ;; Store in reverse lookup map
            (map-set pages-by-title
                { owner: recipient, title: title }
                { id: new-id }
            )

            ;; Assigns page-creator for royalty purposes
            (map-set page-creators new-id recipient) 

            ;; store token-metadata
            (map-set token-metadata
                        new-id
                        {
                            uri-suffix: metadata-uri,  ;; Use the provided IPFS hash
                            version: (+ (default-to u0 (get version (map-get? token-metadata new-id))) u1)
                        }
                    )
            
            ;; Update the page counter
            (var-set last-token-id new-id)
            
            ;; Emit event
            (print {event: "page-airdropped", page-id: new-id, recipient: recipient})
            
            (ok true)
        )
    )
)

Functions (24)

FunctionAccessArgs
get-last-token-idread-only
get-token-uriread-onlytoken-id: uint
transferpublicid: uint, sender: principal, recipient: principal
get-ownerread-onlyid: uint
get-creatorread-onlyid: uint
validate-string-length-titleprivatestr: (string-ascii 64
validate-string-length-descriptionprivatestr: (string-ascii 256
mint-pagepublictitle: (string-ascii 64
is-sender-ownerprivatetoken-id: uint
update-pagepublicid: uint, title: (string-ascii 64
update-feepublicnew-fee: uint
get-page-by-idread-onlypage-id: uint
get-page-by-titleread-onlyowner: principal, title: (string-ascii 64
get-balanceread-onlyaccount: principal
trnsfrprivatetoken-id: uint, sender: principal, recipient: principal
get-listing-in-ustxread-onlyid: uint
list-in-ustxpublicid: uint, price: uint, comm-trait: <commission-trait>
unlist-in-ustxpublicid: uint
buy-in-ustxpublicid: uint, comm-trait: <commission-trait>
get-royalty-percentread-only
set-royalty-percentpublicroyalty: uint
pay-royaltyprivateprice: uint, royalty: uint, id: uint
airdrop-pagespublicrecipients: (list 200 principal
airdrop-single-pageprivaterecipient: principal, title: (string-ascii 64