;; ============================================================
;; 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)
)
)