Source Code

;; ============================================================
;; nova-premium-v1.clar
;; Premium subscription contract for Nova WebApp
;; Accepts USDh payments for monthly/yearly premium plans
;; ============================================================

;; --- Constants ---
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_PAUSED (err u101))
(define-constant ERR_INVALID_PLAN (err u102))
(define-constant ERR_INSUFFICIENT_BALANCE (err u103))
(define-constant ERR_TRANSFER_FAILED (err u104))
(define-constant ERR_ALREADY_ADMIN (err u105))
(define-constant ERR_NOT_ADMIN (err u106))

(define-constant PLAN_MONTHLY u1)
(define-constant PLAN_YEARLY u2)

;; Duration in seconds
(define-constant MONTHLY_SECONDS u2592000)  ;; 30 days
(define-constant YEARLY_SECONDS u31536000)  ;; 365 days

;; --- Data Variables ---
(define-data-var paused bool false)
(define-data-var monthly-price uint u99000000)   ;; 0.99 USDh (8 decimals)
(define-data-var yearly-price uint u999000000)    ;; 9.99 USDh (8 decimals)
(define-data-var total-revenue uint u0)
(define-data-var total-subscriptions uint u0)

;; --- Data Maps ---
(define-map admins principal bool)
(define-map subscriptions
  principal
  {
    plan: uint,
    expiry: uint,
    total-paid: uint,
    last-payment-block: uint,
    subscription-count: uint
  }
)

;; --- Initialize deployer as admin ---
(map-set admins CONTRACT_OWNER true)

;; --- Read-Only Functions ---

(define-read-only (get-subscription (subscriber principal))
  (map-get? subscriptions subscriber)
)

(define-read-only (is-active (subscriber principal))
  (match (map-get? subscriptions subscriber)
    sub (> (get expiry sub) stacks-block-time)
    false
  )
)

(define-read-only (get-monthly-price)
  (var-get monthly-price)
)

(define-read-only (get-yearly-price)
  (var-get yearly-price)
)

(define-read-only (is-paused)
  (var-get paused)
)

(define-read-only (is-admin (account principal))
  (default-to false (map-get? admins account))
)

(define-read-only (get-stats)
  {
    total-revenue: (var-get total-revenue),
    total-subscriptions: (var-get total-subscriptions),
    monthly-price: (var-get monthly-price),
    yearly-price: (var-get yearly-price),
    paused: (var-get paused)
  }
)

;; --- Private Helpers ---

(define-private (is-authorized)
  (or (is-eq tx-sender CONTRACT_OWNER) (default-to false (map-get? admins tx-sender)))
)

(define-private (get-current-time)
  stacks-block-time
)

;; --- Public Functions ---

;; Subscribe to a plan (monthly or yearly)
(define-public (subscribe (plan uint))
  (let
    (
      (price (if (is-eq plan PLAN_MONTHLY)
               (var-get monthly-price)
               (if (is-eq plan PLAN_YEARLY)
                 (var-get yearly-price)
                 u0)))
      (duration (if (is-eq plan PLAN_MONTHLY)
                  MONTHLY_SECONDS
                  (if (is-eq plan PLAN_YEARLY)
                    YEARLY_SECONDS
                    u0)))
      (now (get-current-time))
      (existing (map-get? subscriptions tx-sender))
      (current-expiry (match existing sub (get expiry sub) u0))
      ;; If subscription is still active, extend from current expiry; otherwise from now
      (new-expiry (+ (if (> current-expiry now) current-expiry now) duration))
      (prev-paid (match existing sub (get total-paid sub) u0))
      (prev-count (match existing sub (get subscription-count sub) u0))
    )
    ;; Validations
    (asserts! (not (var-get paused)) ERR_PAUSED)
    (asserts! (or (is-eq plan PLAN_MONTHLY) (is-eq plan PLAN_YEARLY)) ERR_INVALID_PLAN)
    (asserts! (> price u0) ERR_INVALID_PLAN)

    ;; Transfer USDh from subscriber to this contract
    (try! (contract-call?
      'SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.usdh-token-v1
      transfer
      price
      tx-sender
      current-contract
      none
    ))

    ;; Update subscription map
    (map-set subscriptions tx-sender {
      plan: plan,
      expiry: new-expiry,
      total-paid: (+ prev-paid price),
      last-payment-block: stacks-block-height,
      subscription-count: (+ prev-count u1)
    })

    ;; Update global stats
    (var-set total-revenue (+ (var-get total-revenue) price))
    (var-set total-subscriptions (+ (var-get total-subscriptions) u1))

    ;; Emit structured event for Chainhook indexing
    (print {
      event: "subscription",
      subscriber: tx-sender,
      plan: plan,
      amount: price,
      new-expiry: new-expiry,
      block-height: stacks-block-height,
      block-time: now
    })

    (ok {
      plan: plan,
      expiry: new-expiry,
      amount: price
    })
  )
)

;; --- Admin Functions ---

(define-public (set-prices (new-monthly uint) (new-yearly uint))
  (begin
    (asserts! (is-authorized) ERR_NOT_AUTHORIZED)
    (var-set monthly-price new-monthly)
    (var-set yearly-price new-yearly)
    (print { event: "prices-updated", monthly: new-monthly, yearly: new-yearly })
    (ok true)
  )
)

(define-public (set-paused (new-paused bool))
  (begin
    (asserts! (is-authorized) ERR_NOT_AUTHORIZED)
    (var-set paused new-paused)
    (print { event: "paused-updated", paused: new-paused })
    (ok true)
  )
)

(define-public (add-admin (new-admin principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (asserts! (not (default-to false (map-get? admins new-admin))) ERR_ALREADY_ADMIN)
    (map-set admins new-admin true)
    (print { event: "admin-added", admin: new-admin })
    (ok true)
  )
)

(define-public (remove-admin (admin principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (asserts! (default-to false (map-get? admins admin)) ERR_NOT_ADMIN)
    (map-delete admins admin)
    (print { event: "admin-removed", admin: admin })
    (ok true)
  )
)

;; Withdraw accumulated USDh to a specified recipient
(define-public (withdraw (recipient principal) (amount uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)

    ;; Transfer USDh from this contract to recipient
    (try! (contract-call?
      'SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.usdh-token-v1
      transfer
      amount
      current-contract
      recipient
      none
    ))

    (print { event: "withdrawal", recipient: recipient, amount: amount })
    (ok amount)
  )
)

Functions (15)

FunctionAccessArgs
get-subscriptionread-onlysubscriber: principal
is-activeread-onlysubscriber: principal
get-monthly-priceread-only
get-yearly-priceread-only
is-pausedread-only
is-adminread-onlyaccount: principal
get-statsread-only
is-authorizedprivate
get-current-timeprivate
subscribepublicplan: uint
set-pricespublicnew-monthly: uint, new-yearly: uint
set-pausedpublicnew-paused: bool
add-adminpublicnew-admin: principal
remove-adminpublicadmin: principal
withdrawpublicrecipient: principal, amount: uint