Source Code

;; Stacks Receipt of Life is NOTA as a Receipt of Life on Stacks
;; Clarity 4 contract
;; STAMP-FEE is paid to TREASURY on submit (submit-receipt / submit-receipt-for).
;; ROYALTY-FEE is paid to the current royalty-recipient on transfer-receipt.

(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)

;; 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)
       )
    (begin
      ;; fee must succeed before any state change
      (try! (stx-transfer? (var-get STAMP-FEE) tx-sender TREASURY))
      (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))
             )
          (if (not (is-eq tx-sender current-owner))
              ERR-NOT-OWNER
              (begin
                ;; royalty must succeed before state change
                (try! (stx-transfer? (var-get ROYALTY-FEE) tx-sender royalty-to))
                (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)))

Functions (10)

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