Source Code

;; ============================================================================
;; Stacks Receipt of Life v2.0.0
;; On-chain "Receipt of Life" with creator, owner, royalty-recipient, STAMP/ROYALTY
;; fees, paging, and global stats. Superset of v1 (receipt-of-life) with:
;; - Fee safety: skips STX self-transfers (fee = 0 or sender == recipient) so
;;   admin/royalty users don't hit self-transfer failures.
;; - Paging & filtered views: get-receipts-range, and filtered by owner/creator/
;;   royalty-recipient.
;; - Global stats counters for submissions, transfers, and fees actually applied.
;;
;; Economic model
;; - STAMP-FEE (data-var):
;;   * Charged tx-sender -> TREASURY on submit-receipt / submit-receipt-for.
;;   * Skipped when STAMP-FEE == u0 OR tx-sender == TREASURY (self-transfer).
;;   * total-stamp-fee increments only when the fee branch runs.
;; - ROYALTY-FEE (data-var):
;;   * Charged tx-sender (current owner) -> royalty-recipient on transfer-receipt.
;;   * Skipped when ROYALTY-FEE == u0 OR tx-sender == royalty-recipient.
;;   * total-royalty-fee increments only when the fee branch runs.
;; - Creator can update royalty-recipient via set-receipt-royalty-recipient.
;; - Admin (admin data-var) can call set-fees and set-admin.
;;
;; Public API (write):
;; - submit-receipt(text)
;; - submit-receipt-for(text, recipient)
;; - transfer-receipt(id, new-owner)
;; - set-receipt-royalty-recipient(id, new-recipient)
;; - set-fees(new-stamp-fee, new-royalty-fee)
;; - set-admin(new-admin)
;; Public API (read-only):
;; - get-receipt(id)
;; - get-receipts-range(start-id, limit)
;; - get-receipts-by-owner(owner, start-id, limit)
;; - get-receipts-by-creator(creator, start-id, limit)
;; - get-receipts-by-royalty-recipient(recipient, start-id, limit)
;; - get-last-id()
;; - get-version()
;; - get-config()
;; - get-stats()
;; ============================================================================

(define-constant VERSION-MAJOR u2)
(define-constant VERSION-MINOR u0)
(define-constant VERSION-PATCH u0)

(define-constant MAX-PAGE-SIZE u10)

(define-constant TREASURY 'SP29ECHHQ6F9344SGGGRGDPTPFPTXA3GHXK28KCWH)

;; contract owner and mutable admin
(define-constant CONTRACT-OWNER 'SP29ECHHQ6F9344SGGGRGDPTPFPTXA3GHXK28KCWH)
;; Mutable admin; default = contract deployer (in this case, same as CONTRACT-OWNER)
(define-data-var admin principal CONTRACT-OWNER)

;; Fees (mutable)
(define-data-var STAMP-FEE uint u1000)
(define-data-var ROYALTY-FEE uint u500)

;; Stats
(define-data-var total-submissions uint u0)
(define-data-var total-transfers uint u0)
(define-data-var total-stamp-fee uint u0)
(define-data-var total-royalty-fee uint u0)

;; Errors
(define-constant ERR-NOT-OWNER (err u401))
(define-constant ERR-NOT-CREATOR (err u402))
(define-constant ERR-NOT-AUTHORIZED (err u403))
(define-constant ERR-NOT-FOUND (err u404))

;; Incremental identifier for receipts
(define-data-var last-id uint u0)

;; Stored receipts:
;; - id: auto-incremented uint
;; - creator: principal that submitted the receipt originally
;; - owner: current principal that holds the receipt
;; - royalty-recipient: principal that receives transfer royalties (default: creator)
;; - text: the "Receipt of Life" message (max 160 chars)
;; - created-at: timestamp of the Stacks block when it was submitted
(define-map receipts
  { id: uint }
  {
    creator: principal,
    owner: principal,
    royalty-recipient: principal,
    text: (string-utf8 160),
    created-at: uint
  }
)

;; Admin helper
(define-private (is-admin (who principal))
  (is-eq who (var-get admin))
)

;; Public: change admin address (admin-only)
(define-public (set-admin (new-admin principal))
  (if (not (is-admin tx-sender))
      ERR-NOT-AUTHORIZED
      (begin
        (var-set admin new-admin)
        (ok new-admin)
      )))

;; Internal helper to insert a new receipt with given owner (fee first)
(define-private (insert-receipt (text (string-utf8 160)) (owner principal))
  (let (
        (new-id (+ (var-get last-id) u1))
        (now    stacks-block-time)
       (stamp-fee (var-get STAMP-FEE))
        (stamp-fee-applies (and (> stamp-fee u0) (not (is-eq tx-sender TREASURY))))
       )
    (begin
      ;; fee must succeed before any state change
      (unwrap-panic
        (if stamp-fee-applies
            (begin
              (try! (stx-transfer? stamp-fee tx-sender TREASURY))
              (var-set total-stamp-fee (+ (var-get total-stamp-fee) stamp-fee))
              (ok true))
            (ok true)))
      (var-set total-submissions (+ (var-get total-submissions) u1))
      (var-set last-id new-id)
      (map-insert receipts
        { id: new-id }
        {
          creator: tx-sender,
          owner: owner,
          royalty-recipient: tx-sender,
          text: text,
          created-at: now
        })
      (print
        {
          kind: "receipt-submitted",
          id: new-id,
          creator: tx-sender,
          owner: owner,
          royalty-recipient: tx-sender,
          created-at: now
        })
      (ok new-id)
    )))

;; Public: self-stamp (creator = tx-sender, owner = tx-sender)
(define-public (submit-receipt (text (string-utf8 160)))
  (insert-receipt text tx-sender))

;; Public: stamp for another principal (creator = tx-sender, owner = recipient)
(define-public (submit-receipt-for (text (string-utf8 160)) (recipient principal))
  (insert-receipt text recipient))

;; Public: transfer ownership to a new owner; creator stays unchanged; pays royalty first
(define-public (transfer-receipt (id uint) (new-owner principal))
  (let ((entry (map-get? receipts { id: id })))
    (if (is-none entry)
        ERR-NOT-FOUND
        (let (
              (receipt (unwrap! entry ERR-NOT-FOUND))
              (current-owner (get owner receipt))
              (royalty-to (get royalty-recipient receipt))
              (royalty-fee (var-get ROYALTY-FEE))
              (royalty-applies (and (> royalty-fee u0) (not (is-eq tx-sender royalty-to))))
             )
          (if (not (is-eq tx-sender current-owner))
              ERR-NOT-OWNER
              (begin
                (unwrap-panic
                  (if royalty-applies
                      (begin
                        (try! (stx-transfer? royalty-fee tx-sender royalty-to))
                        (var-set total-royalty-fee (+ (var-get total-royalty-fee) royalty-fee))
                        (ok true))
                      (ok true)))
                (var-set total-transfers (+ (var-get total-transfers) u1))
                (map-set receipts { id: id }
                  {
                    creator: (get creator receipt),
                    owner: new-owner,
                    royalty-recipient: royalty-to,
                    text: (get text receipt),
                    created-at: (get created-at receipt)
                  })
                (print
                  {
                    kind: "receipt-transferred",
                    id: id,
                    from: current-owner,
                    to: new-owner,
                    royalty-to: royalty-to
                  })
                (ok id)))))))

;; Public: creator-only change of royalty recipient for a receipt
(define-public (set-receipt-royalty-recipient (id uint) (new-recipient principal))
  (let ((entry (map-get? receipts { id: id })))
    (if (is-none entry)
        ERR-NOT-FOUND
        (let ((receipt (unwrap! entry ERR-NOT-FOUND)))
          (if (not (is-eq tx-sender (get creator receipt)))
              ERR-NOT-CREATOR
              (begin
                (map-set receipts { id: id }
                  {
                    creator: (get creator receipt),
                    owner: (get owner receipt),
                    royalty-recipient: new-recipient,
                    text: (get text receipt),
                    created-at: (get created-at receipt)
                  })
                (print
                  {
                    kind: "receipt-royalty-updated",
                    id: id,
                    creator: (get creator receipt),
                    new-recipient: new-recipient
                  })
                (ok id)
              ))))))

;; Public: admin-only update of fees
(define-public (set-fees (new-stamp-fee uint) (new-royalty-fee uint))
  (if (not (is-admin tx-sender))
      ERR-NOT-AUTHORIZED
      (begin
        (var-set STAMP-FEE new-stamp-fee)
        (var-set ROYALTY-FEE new-royalty-fee)
        (ok
          {
            stamp-fee: (var-get STAMP-FEE),
            royalty-fee: (var-get ROYALTY-FEE)
          })
      )))

;; Read-only helper: get a receipt by id
(define-read-only (get-receipt (id uint))
  (map-get? receipts { id: id }))

;; Read-only helper: get the current last-id
(define-read-only (get-last-id)
  (ok (var-get last-id)))

;; Internal helper: collect receipt into list if present
(define-private (collect-receipt
  (id uint)
  (acc (list 10 (tuple
                  (id uint)
                  (creator principal)
                  (owner principal)
                  (royalty-recipient principal)
                  (text (string-utf8 160))
                  (created-at uint)))))
  (let ((entry (map-get? receipts { id: id })))
    (if (is-none entry)
        acc
        (let ((receipt (unwrap-panic entry)))
          (if (>= (len acc) MAX-PAGE-SIZE)
              acc
              (match
                (as-max-len?
                  (append acc {
                    id: id,
                    creator: (get creator receipt),
                    owner: (get owner receipt),
                    royalty-recipient: (get royalty-recipient receipt),
                    text: (get text receipt),
                    created-at: (get created-at receipt)
                  })
                  u10)
                result
                result
                acc))))))

;; Read-only: paged receipts scan
(define-read-only (get-receipts-range (start-id uint) (limit uint))
  (let ((last (var-get last-id)))
    (if (or (is-eq last u0) (is-eq start-id u0) (> start-id last))
        (ok (list))
        (let (
              (effective-limit (if (< limit MAX-PAGE-SIZE) limit MAX-PAGE-SIZE))
             )
          (let (
                (list0 (unwrap-panic (as-max-len? (list) u10)))
                (list1 (if (and (> effective-limit u0) (<= start-id last))
                           (collect-receipt start-id list0)
                           list0))
                (id2 (+ start-id u1))
                (list2 (if (and (> effective-limit u1) (<= id2 last))
                           (collect-receipt id2 list1)
                           list1))
                (id3 (+ start-id u2))
                (list3 (if (and (> effective-limit u2) (<= id3 last))
                           (collect-receipt id3 list2)
                           list2))
                (id4 (+ start-id u3))
                (list4 (if (and (> effective-limit u3) (<= id4 last))
                           (collect-receipt id4 list3)
                           list3))
                (id5 (+ start-id u4))
                (list5 (if (and (> effective-limit u4) (<= id5 last))
                           (collect-receipt id5 list4)
                           list4))
                (id6 (+ start-id u5))
                (list6 (if (and (> effective-limit u5) (<= id6 last))
                           (collect-receipt id6 list5)
                           list5))
                (id7 (+ start-id u6))
                (list7 (if (and (> effective-limit u6) (<= id7 last))
                           (collect-receipt id7 list6)
                           list6))
                (id8 (+ start-id u7))
                (list8 (if (and (> effective-limit u7) (<= id8 last))
                           (collect-receipt id8 list7)
                           list7))
                (id9 (+ start-id u8))
                (list9 (if (and (> effective-limit u8) (<= id9 last))
                           (collect-receipt id9 list8)
                           list8))
                (id10 (+ start-id u9))
                (list10 (if (and (> effective-limit u9) (<= id10 last))
                            (collect-receipt id10 list9)
                            list9))
               )
            (ok list10))))))

;; Read-only: contract version
(define-read-only (get-version)
  (ok
    {
      major: VERSION-MAJOR,
      minor: VERSION-MINOR,
      patch: VERSION-PATCH
    }))

;; Read-only: config snapshot
(define-read-only (get-config)
  (ok
    {
      contract-owner: CONTRACT-OWNER,
      treasury: TREASURY,
      admin: (var-get admin),
      stamp-fee: (var-get STAMP-FEE),
      royalty-fee: (var-get ROYALTY-FEE),
      last-id: (var-get last-id),
      version-major: VERSION-MAJOR,
      version-minor: VERSION-MINOR,
      version-patch: VERSION-PATCH
    }))

(define-read-only (get-stats)
  (ok
    {
      major: VERSION-MAJOR,
      minor: VERSION-MINOR,
      patch: VERSION-PATCH,
      last-id: (var-get last-id),
      total-submissions: (var-get total-submissions),
      total-transfers: (var-get total-transfers),
      total-stamp-fee: (var-get total-stamp-fee),
      total-royalty-fee: (var-get total-royalty-fee)
    }))

(define-private (add-if-owner
  (id uint)
  (target principal)
  (acc (list 10 (tuple
                  (id uint)
                  (creator principal)
                  (owner principal)
                  (royalty-recipient principal)
                  (text (string-utf8 160))
                  (created-at uint)))))
  (match (map-get? receipts { id: id })
    receipt (if (and (< (len acc) MAX-PAGE-SIZE) (is-eq target (get owner receipt)))
                (match
                  (as-max-len?
                    (append acc {
                      id: id,
                      creator: (get creator receipt),
                      owner: (get owner receipt),
                      royalty-recipient: (get royalty-recipient receipt),
                      text: (get text receipt),
                      created-at: (get created-at receipt)
                    })
                    u10)
                  result result
                  acc)
                acc)
    acc))

(define-private (add-if-creator
  (id uint)
  (target principal)
  (acc (list 10 (tuple
                  (id uint)
                  (creator principal)
                  (owner principal)
                  (royalty-recipient principal)
                  (text (string-utf8 160))
                  (created-at uint)))))
  (match (map-get? receipts { id: id })
    receipt (if (and (< (len acc) MAX-PAGE-SIZE) (is-eq target (get creator receipt)))
                (match
                  (as-max-len?
                    (append acc {
                      id: id,
                      creator: (get creator receipt),
                      owner: (get owner receipt),
                      royalty-recipient: (get royalty-recipient receipt),
                      text: (get text receipt),
                      created-at: (get created-at receipt)
                    })
                    u10)
                  result result
                  acc)
                acc)
    acc))

(define-private (add-if-royalty
  (id uint)
  (target principal)
  (acc (list 10 (tuple
                  (id uint)
                  (creator principal)
                  (owner principal)
                  (royalty-recipient principal)
                  (text (string-utf8 160))
                  (created-at uint)))))
  (match (map-get? receipts { id: id })
    receipt (if (and (< (len acc) MAX-PAGE-SIZE) (is-eq target (get royalty-recipient receipt)))
                (match
                  (as-max-len?
                    (append acc {
                      id: id,
                      creator: (get creator receipt),
                      owner: (get owner receipt),
                      royalty-recipient: (get royalty-recipient receipt),
                      text: (get text receipt),
                      created-at: (get created-at receipt)
                    })
                    u10)
                  result result
                  acc)
                acc)
    acc))

(define-read-only (get-receipts-by-owner (owner principal) (start-id uint) (limit uint))
  (let (
        (last (var-get last-id))
        (normalized-start (if (is-eq start-id u0) u1 start-id))
       )
    (if (or (is-eq limit u0) (is-eq last u0) (> normalized-start last))
        (ok (list))
        (let (
              (effective-limit (if (< limit MAX-PAGE-SIZE) limit MAX-PAGE-SIZE))
              (list0 (unwrap-panic (as-max-len? (list) u10)))
              (list1 (if (and (> effective-limit u0) (<= normalized-start last))
                         (add-if-owner normalized-start owner list0)
                         list0))
              (id2 (+ normalized-start u1))
              (list2 (if (and (> effective-limit u1) (<= id2 last))
                         (add-if-owner id2 owner list1)
                         list1))
              (id3 (+ normalized-start u2))
              (list3 (if (and (> effective-limit u2) (<= id3 last))
                         (add-if-owner id3 owner list2)
                         list2))
              (id4 (+ normalized-start u3))
              (list4 (if (and (> effective-limit u3) (<= id4 last))
                         (add-if-owner id4 owner list3)
                         list3))
              (id5 (+ normalized-start u4))
              (list5 (if (and (> effective-limit u4) (<= id5 last))
                         (add-if-owner id5 owner list4)
                         list4))
              (id6 (+ normalized-start u5))
              (list6 (if (and (> effective-limit u5) (<= id6 last))
                         (add-if-owner id6 owner list5)
                         list5))
              (id7 (+ normalized-start u6))
              (list7 (if (and (> effective-limit u6) (<= id7 last))
                         (add-if-owner id7 owner list6)
                         list6))
              (id8 (+ normalized-start u7))
              (list8 (if (and (> effective-limit u7) (<= id8 last))
                         (add-if-owner id8 owner list7)
                         list7))
              (id9 (+ normalized-start u8))
              (list9 (if (and (> effective-limit u8) (<= id9 last))
                         (add-if-owner id9 owner list8)
                         list8))
              (id10 (+ normalized-start u9))
              (list10 (if (and (> effective-limit u9) (<= id10 last))
                          (add-if-owner id10 owner list9)
                          list9))
             )
          (ok list10)))))

(define-read-only (get-receipts-by-creator (creator principal) (start-id uint) (limit uint))
  (let (
        (last (var-get last-id))
        (normalized-start (if (is-eq start-id u0) u1 start-id))
       )
    (if (or (is-eq limit u0) (is-eq last u0) (> normalized-start last))
        (ok (list))
        (let (
              (effective-limit (if (< limit MAX-PAGE-SIZE) limit MAX-PAGE-SIZE))
              (list0 (unwrap-panic (as-max-len? (list) u10)))
              (list1 (if (and (> effective-limit u0) (<= normalized-start last))
                         (add-if-creator normalized-start creator list0)
                         list0))
              (id2 (+ normalized-start u1))
              (list2 (if (and (> effective-limit u1) (<= id2 last))
                         (add-if-creator id2 creator list1)
                         list1))
              (id3 (+ normalized-start u2))
              (list3 (if (and (> effective-limit u2) (<= id3 last))
                         (add-if-creator id3 creator list2)
                         list2))
              (id4 (+ normalized-start u3))
              (list4 (if (and (> effective-limit u3) (<= id4 last))
                         (add-if-creator id4 creator list3)
                         list3))
              (id5 (+ normalized-start u4))
              (list5 (if (and (> effective-limit u4) (<= id5 last))
                         (add-if-creator id5 creator list4)
                         list4))
              (id6 (+ normalized-start u5))
              (list6 (if (and (> effective-limit u5) (<= id6 last))
                         (add-if-creator id6 creator list5)
                         list5))
              (id7 (+ normalized-start u6))
              (list7 (if (and (> effective-limit u6) (<= id7 last))
                         (add-if-creator id7 creator list6)
                         list6))
              (id8 (+ normalized-start u7))
              (list8 (if (and (> effective-limit u7) (<= id8 last))
                         (add-if-creator id8 creator list7)
                         list7))
              (id9 (+ normalized-start u8))
              (list9 (if (and (> effective-limit u8) (<= id9 last))
                         (add-if-creator id9 creator list8)
                         list8))
              (id10 (+ normalized-start u9))
              (list10 (if (and (> effective-limit u9) (<= id10 last))
                          (add-if-creator id10 creator list9)
                          list9))
             )
          (ok list10)))))

(define-read-only (get-receipts-by-royalty-recipient (recipient principal) (start-id uint) (limit uint))
  (let (
        (last (var-get last-id))
        (normalized-start (if (is-eq start-id u0) u1 start-id))
       )
    (if (or (is-eq limit u0) (is-eq last u0) (> normalized-start last))
        (ok (list))
        (let (
              (effective-limit (if (< limit MAX-PAGE-SIZE) limit MAX-PAGE-SIZE))
              (list0 (unwrap-panic (as-max-len? (list) u10)))
              (list1 (if (and (> effective-limit u0) (<= normalized-start last))
                         (add-if-royalty normalized-start recipient list0)
                         list0))
              (id2 (+ normalized-start u1))
              (list2 (if (and (> effective-limit u1) (<= id2 last))
                         (add-if-royalty id2 recipient list1)
                         list1))
              (id3 (+ normalized-start u2))
              (list3 (if (and (> effective-limit u2) (<= id3 last))
                         (add-if-royalty id3 recipient list2)
                         list2))
              (id4 (+ normalized-start u3))
              (list4 (if (and (> effective-limit u3) (<= id4 last))
                         (add-if-royalty id4 recipient list3)
                         list3))
              (id5 (+ normalized-start u4))
              (list5 (if (and (> effective-limit u4) (<= id5 last))
                         (add-if-royalty id5 recipient list4)
                         list4))
              (id6 (+ normalized-start u5))
              (list6 (if (and (> effective-limit u5) (<= id6 last))
                         (add-if-royalty id6 recipient list5)
                         list5))
              (id7 (+ normalized-start u6))
              (list7 (if (and (> effective-limit u6) (<= id7 last))
                         (add-if-royalty id7 recipient list6)
                         list6))
              (id8 (+ normalized-start u7))
              (list8 (if (and (> effective-limit u7) (<= id8 last))
                         (add-if-royalty id8 recipient list7)
                         list7))
              (id9 (+ normalized-start u8))
              (list9 (if (and (> effective-limit u8) (<= id9 last))
                         (add-if-royalty id9 recipient list8)
                         list8))
              (id10 (+ normalized-start u9))
              (list10 (if (and (> effective-limit u9) (<= id10 last))
                          (add-if-royalty id10 recipient list9)
                          list9))
             )
          (ok list10)))))

Functions (21)

FunctionAccessArgs
is-adminprivatewho: principal
set-adminpublicnew-admin: principal
insert-receiptprivatetext: (string-utf8 160
submit-receiptpublictext: (string-utf8 160
submit-receipt-forpublictext: (string-utf8 160
transfer-receiptpublicid: uint, new-owner: principal
set-receipt-royalty-recipientpublicid: uint, new-recipient: principal
set-feespublicnew-stamp-fee: uint, new-royalty-fee: uint
get-receiptread-onlyid: uint
get-last-idread-only
collect-receiptprivateid: uint, acc: (list 10 (tuple (id uint, creator: principal, owner: principal, royalty-recipient: principal, text: (string-utf8 160
get-receipts-rangeread-onlystart-id: uint, limit: uint
get-versionread-only
get-configread-only
get-statsread-only
add-if-ownerprivateid: uint, target: principal, acc: (list 10 (tuple (id uint, creator: principal, owner: principal, royalty-recipient: principal, text: (string-utf8 160
add-if-creatorprivateid: uint, target: principal, acc: (list 10 (tuple (id uint, creator: principal, owner: principal, royalty-recipient: principal, text: (string-utf8 160
add-if-royaltyprivateid: uint, target: principal, acc: (list 10 (tuple (id uint, creator: principal, owner: principal, royalty-recipient: principal, text: (string-utf8 160
get-receipts-by-ownerread-onlyowner: principal, start-id: uint, limit: uint
get-receipts-by-creatorread-onlycreator: principal, start-id: uint, limit: uint
get-receipts-by-royalty-recipientread-onlyrecipient: principal, start-id: uint, limit: uint