Source Code

;; TipStream Subscriptions
;; Recurring patronage payments from subscribers to content creators
;; Subscribers control their own payment triggers

(define-constant err-invalid-amount (err u500))
(define-constant err-not-found (err u501))
(define-constant err-not-authorized (err u502))
(define-constant err-too-early (err u503))
(define-constant err-already-cancelled (err u504))

(define-data-var subscription-nonce uint u0)

(define-map subscriptions
    { sub-id: uint }
    {
        subscriber: principal,
        creator: principal,
        amount: uint,
        interval-blocks: uint,
        last-payment-height: uint,
        total-paid: uint,
        payments-made: uint,
        active: bool
    }
)

(define-map subscriber-sub-count principal uint)
(define-map creator-sub-count principal uint)

;; Create a subscription with an immediate first payment
(define-public (create-subscription (creator principal) (amount uint) (interval-blocks uint))
    (let
        (
            (sub-id (var-get subscription-nonce))
            (sub-count (default-to u0 (map-get? subscriber-sub-count tx-sender)))
            (creator-subs (default-to u0 (map-get? creator-sub-count creator)))
        )
        (asserts! (> amount u0) err-invalid-amount)
        (asserts! (> interval-blocks u0) err-invalid-amount)
        (asserts! (not (is-eq tx-sender creator)) err-invalid-amount)
        ;; First payment
        (try! (stx-transfer? amount tx-sender creator))
        (map-set subscriptions
            { sub-id: sub-id }
            {
                subscriber: tx-sender,
                creator: creator,
                amount: amount,
                interval-blocks: interval-blocks,
                last-payment-height: block-height,
                total-paid: amount,
                payments-made: u1,
                active: true
            }
        )
        (map-set subscriber-sub-count tx-sender (+ sub-count u1))
        (map-set creator-sub-count creator (+ creator-subs u1))
        (var-set subscription-nonce (+ sub-id u1))
        (ok sub-id)
    )
)

;; Process a recurring payment (subscriber calls this when interval has elapsed)
(define-public (process-payment (sub-id uint))
    (let
        (
            (sub (unwrap! (map-get? subscriptions { sub-id: sub-id }) err-not-found))
            (amount (get amount sub))
            (last-payment (get last-payment-height sub))
            (interval (get interval-blocks sub))
        )
        (asserts! (get active sub) err-already-cancelled)
        (asserts! (is-eq tx-sender (get subscriber sub)) err-not-authorized)
        (asserts! (>= block-height (+ last-payment interval)) err-too-early)
        (try! (stx-transfer? amount tx-sender (get creator sub)))
        (map-set subscriptions
            { sub-id: sub-id }
            (merge sub {
                last-payment-height: block-height,
                total-paid: (+ (get total-paid sub) amount),
                payments-made: (+ (get payments-made sub) u1)
            })
        )
        (ok true)
    )
)

;; Cancel subscription (subscriber only)
(define-public (cancel-subscription (sub-id uint))
    (let
        (
            (sub (unwrap! (map-get? subscriptions { sub-id: sub-id }) err-not-found))
        )
        (asserts! (is-eq tx-sender (get subscriber sub)) err-not-authorized)
        (asserts! (get active sub) err-already-cancelled)
        (map-set subscriptions
            { sub-id: sub-id }
            (merge sub { active: false })
        )
        (ok true)
    )
)

;; Update subscription amount (subscriber only)
(define-public (update-amount (sub-id uint) (new-amount uint))
    (let
        (
            (sub (unwrap! (map-get? subscriptions { sub-id: sub-id }) err-not-found))
        )
        (asserts! (is-eq tx-sender (get subscriber sub)) err-not-authorized)
        (asserts! (get active sub) err-already-cancelled)
        (asserts! (> new-amount u0) err-invalid-amount)
        (map-set subscriptions
            { sub-id: sub-id }
            (merge sub { amount: new-amount })
        )
        (ok true)
    )
)

;; ---------- Read-only ----------

(define-read-only (get-subscription (sub-id uint))
    (map-get? subscriptions { sub-id: sub-id })
)

(define-read-only (get-subscription-count)
    (var-get subscription-nonce)
)

(define-read-only (get-subscriber-count (user principal))
    (default-to u0 (map-get? subscriber-sub-count user))
)

(define-read-only (get-creator-subscribers (creator principal))
    (default-to u0 (map-get? creator-sub-count creator))
)

(define-read-only (is-payment-due (sub-id uint))
    (match (map-get? subscriptions { sub-id: sub-id })
        sub (and (get active sub)
                 (>= block-height (+ (get last-payment-height sub) (get interval-blocks sub))))
        false
    )
)

Functions (9)

FunctionAccessArgs
create-subscriptionpubliccreator: principal, amount: uint, interval-blocks: uint
process-paymentpublicsub-id: uint
cancel-subscriptionpublicsub-id: uint
update-amountpublicsub-id: uint, new-amount: uint
get-subscriptionread-onlysub-id: uint
get-subscription-countread-only
get-subscriber-countread-onlyuser: principal
get-creator-subscribersread-onlycreator: principal
is-payment-dueread-onlysub-id: uint