Source Code

;; Alert Manager Contract
;; Create, manage, and configure custom blockchain alerts
;; Built with Clarity 4 features for Stacks Builder Challenge

;; ============================================
;; CONSTANTS & ERRORS
;; ============================================

(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u200))
(define-constant ERR-ALERT-NOT-FOUND (err u201))
(define-constant ERR-ALERT-LIMIT-REACHED (err u202))
(define-constant ERR-INVALID-ALERT-TYPE (err u203))
(define-constant ERR-ALERT-ALREADY-EXISTS (err u204))
(define-constant ERR-INSUFFICIENT-BALANCE (err u205))
(define-constant ERR-NOT-REGISTERED (err u206))
(define-constant ERR-INVALID-THRESHOLD (err u207))
(define-constant ERR-SUBSCRIPTION-REQUIRED (err u208))

;; Alert types
(define-constant ALERT-TYPE-WHALE-TRANSFER u1)
(define-constant ALERT-TYPE-CONTRACT-DEPLOY u2)
(define-constant ALERT-TYPE-NFT-MINT u3)
(define-constant ALERT-TYPE-TOKEN-LAUNCH u4)
(define-constant ALERT-TYPE-LARGE-SWAP u5)
(define-constant ALERT-TYPE-PRICE-THRESHOLD u6)
(define-constant ALERT-TYPE-CONTRACT-CALL u7)
(define-constant ALERT-TYPE-CUSTOM u8)

;; Alert creation fee (in microSTX)
(define-constant ALERT-CREATION-FEE u1000000) ;; 1 STX per alert

;; ============================================
;; DATA VARIABLES
;; ============================================

(define-data-var alert-nonce uint u0)
(define-data-var total-alerts-created uint u0)
(define-data-var total-alerts-triggered uint u0)
(define-data-var total-fees-collected uint u0)
(define-data-var fee-vault-principal principal 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)

;; ============================================
;; DATA MAPS
;; ============================================

;; Main alert storage
(define-map alerts
  uint  ;; alert-id
  {
    owner: principal,
    alert-type: uint,
    name: (string-ascii 64),
    description: (string-utf8 256),
    threshold: uint,
    target-contract: (optional principal),
    target-function: (optional (string-ascii 128)),
    webhook-url: (optional (string-ascii 256)),
    is-active: bool,
    created-at: uint,
    triggered-count: uint,
    last-triggered: uint
  }
)

;; User's alert IDs
(define-map user-alerts
  principal
  (list 100 uint)
)

;; Track which contracts are being monitored
(define-map monitored-contracts
  principal
  {
    monitor-count: uint,
    first-monitored: uint
  }
)

;; Alert type statistics
(define-map alert-type-stats
  uint
  {
    total-created: uint,
    total-triggered: uint
  }
)

;; ============================================
;; READ-ONLY FUNCTIONS
;; ============================================

;; Get alert by ID
(define-read-only (get-alert (alert-id uint))
  (map-get? alerts alert-id)
)

;; Get user's alerts
(define-read-only (get-user-alerts (user principal))
  (default-to (list) (map-get? user-alerts user))
)

;; Get user's alert count
(define-read-only (get-user-alert-count (user principal))
  (len (default-to (list) (map-get? user-alerts user)))
)

;; Get platform statistics
(define-read-only (get-stats)
  {
    total-alerts: (var-get total-alerts-created),
    total-triggered: (var-get total-alerts-triggered),
    total-fees: (var-get total-fees-collected),
    current-nonce: (var-get alert-nonce)
  }
)

;; Get alert type stats
(define-read-only (get-alert-type-stats (alert-type uint))
  (default-to 
    { total-created: u0, total-triggered: u0 }
    (map-get? alert-type-stats alert-type)
  )
)

;; Check if alert type is valid
(define-read-only (is-valid-alert-type (alert-type uint))
  (and (>= alert-type u1) (<= alert-type u8))
)

;; Get monitored contract info
(define-read-only (get-monitored-contract (contract-principal principal))
  (map-get? monitored-contracts contract-principal)
)

;; ============================================
;; PRIVATE FUNCTIONS
;; ============================================

;; Update alert type statistics
(define-private (update-alert-type-created (alert-type uint))
  (let
    (
      (current-stats (default-to { total-created: u0, total-triggered: u0 } 
                      (map-get? alert-type-stats alert-type)))
    )
    (map-set alert-type-stats alert-type
      (merge current-stats {
        total-created: (+ (get total-created current-stats) u1)
      })
    )
  )
)

(define-private (update-alert-type-triggered (alert-type uint))
  (let
    (
      (current-stats (default-to { total-created: u0, total-triggered: u0 } 
                      (map-get? alert-type-stats alert-type)))
    )
    (map-set alert-type-stats alert-type
      (merge current-stats {
        total-triggered: (+ (get total-triggered current-stats) u1)
      })
    )
  )
)

;; ============================================
;; PUBLIC FUNCTIONS
;; ============================================

;; Create a new alert
(define-public (create-alert 
    (alert-type uint)
    (name (string-ascii 64))
    (description (string-utf8 256))
    (threshold uint)
    (target-contract (optional principal))
    (target-function (optional (string-ascii 128)))
    (webhook-url (optional (string-ascii 256)))
  )
  (let
    (
      (caller tx-sender)
      (alert-id (+ (var-get alert-nonce) u1))
      (user-current-alerts (default-to (list) (map-get? user-alerts caller)))
    )
    ;; Validate alert type
    (asserts! (is-valid-alert-type alert-type) ERR-INVALID-ALERT-TYPE)
    
    ;; Validate threshold (must be > 0 for whale transfers, swaps, etc.)
    (asserts! (or (> threshold u0) (is-eq alert-type ALERT-TYPE-CONTRACT-DEPLOY) (is-eq alert-type ALERT-TYPE-TOKEN-LAUNCH)) ERR-INVALID-THRESHOLD)
    
    ;; Check user is registered and can create alerts
    (asserts! (contract-call? .stackpulse-registry can-create-alert caller) ERR-ALERT-LIMIT-REACHED)
    
    ;; Charge creation fee
    (try! (stx-transfer? ALERT-CREATION-FEE caller (var-get fee-vault-principal)))
    
    ;; Create the alert
    (map-set alerts alert-id
      {
        owner: caller,
        alert-type: alert-type,
        name: name,
        description: description,
        threshold: threshold,
        target-contract: target-contract,
        target-function: target-function,
        webhook-url: webhook-url,
        is-active: true,
        created-at: stacks-block-height,
        triggered-count: u0,
        last-triggered: u0
      }
    )
    
    ;; Add to user's alerts list - Clarity 4 enhanced list operations
    (map-set user-alerts caller
      (unwrap! (as-max-len? (append user-current-alerts alert-id) u100) ERR-ALERT-LIMIT-REACHED)
    )
    
    ;; Update monitored contracts if applicable
    (match target-contract
      contract-to-monitor 
        (let
          (
            (current-monitor (default-to { monitor-count: u0, first-monitored: stacks-block-height } 
                              (map-get? monitored-contracts contract-to-monitor)))
          )
          (map-set monitored-contracts contract-to-monitor
            (merge current-monitor {
              monitor-count: (+ (get monitor-count current-monitor) u1)
            })
          )
        )
      true
    )
    
    ;; Update counters
    (var-set alert-nonce alert-id)
    (var-set total-alerts-created (+ (var-get total-alerts-created) u1))
    (var-set total-fees-collected (+ (var-get total-fees-collected) ALERT-CREATION-FEE))
    
    ;; Update alert type stats
    (update-alert-type-created alert-type)
    
    ;; Notify registry contract
    (try! (contract-call? .stackpulse-registry increment-alerts caller))
    
    ;; Emit event for chainhook
    (print {
      event: "alert-created",
      alert-id: alert-id,
      owner: caller,
      alert-type: alert-type,
      name: name,
      threshold: threshold,
      target-contract: target-contract,
      block: stacks-block-height
    })
    
    (ok alert-id)
  )
)

;; Toggle alert active status
(define-public (toggle-alert (alert-id uint))
  (let
    (
      (caller tx-sender)
      (alert-data (unwrap! (map-get? alerts alert-id) ERR-ALERT-NOT-FOUND))
    )
    ;; Only owner can toggle
    (asserts! (is-eq (get owner alert-data) caller) ERR-NOT-AUTHORIZED)
    
    (map-set alerts alert-id
      (merge alert-data {
        is-active: (not (get is-active alert-data))
      })
    )
    
    (print {
      event: "alert-toggled",
      alert-id: alert-id,
      is-active: (not (get is-active alert-data)),
      block: stacks-block-height
    })
    
    (ok (not (get is-active alert-data)))
  )
)

;; Update alert threshold
(define-public (update-threshold (alert-id uint) (new-threshold uint))
  (let
    (
      (caller tx-sender)
      (alert-data (unwrap! (map-get? alerts alert-id) ERR-ALERT-NOT-FOUND))
    )
    (asserts! (is-eq (get owner alert-data) caller) ERR-NOT-AUTHORIZED)
    (asserts! (> new-threshold u0) ERR-INVALID-THRESHOLD)
    
    (map-set alerts alert-id
      (merge alert-data { threshold: new-threshold })
    )
    
    (print {
      event: "alert-threshold-updated",
      alert-id: alert-id,
      old-threshold: (get threshold alert-data),
      new-threshold: new-threshold,
      block: stacks-block-height
    })
    
    (ok true)
  )
)

;; Update webhook URL (requires subscription)
(define-public (update-webhook (alert-id uint) (new-webhook (optional (string-ascii 256))))
  (let
    (
      (caller tx-sender)
      (alert-data (unwrap! (map-get? alerts alert-id) ERR-ALERT-NOT-FOUND))
    )
    (asserts! (is-eq (get owner alert-data) caller) ERR-NOT-AUTHORIZED)
    (asserts! (contract-call? .stackpulse-registry is-subscription-active caller) ERR-SUBSCRIPTION-REQUIRED)
    
    (map-set alerts alert-id
      (merge alert-data { webhook-url: new-webhook })
    )
    
    (print {
      event: "alert-webhook-updated",
      alert-id: alert-id,
      block: stacks-block-height
    })
    
    (ok true)
  )
)

;; Record alert triggered (called by admin/oracle)
(define-public (record-trigger (alert-id uint))
  (let
    (
      (alert-data (unwrap! (map-get? alerts alert-id) ERR-ALERT-NOT-FOUND))
    )
    ;; Only contract owner can record triggers
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    
    ;; Update alert
    (map-set alerts alert-id
      (merge alert-data {
        triggered-count: (+ (get triggered-count alert-data) u1),
        last-triggered: stacks-block-height
      })
    )
    
    ;; Update global counter
    (var-set total-alerts-triggered (+ (var-get total-alerts-triggered) u1))
    
    ;; Update type stats
    (update-alert-type-triggered (get alert-type alert-data))
    
    ;; Emit trigger event for chainhook
    (print {
      event: "alert-triggered",
      alert-id: alert-id,
      owner: (get owner alert-data),
      alert-type: (get alert-type alert-data),
      triggered-count: (+ (get triggered-count alert-data) u1),
      block: stacks-block-height
    })
    
    (ok true)
  )
)

;; Delete alert
(define-public (delete-alert (alert-id uint))
  (let
    (
      (caller tx-sender)
      (alert-data (unwrap! (map-get? alerts alert-id) ERR-ALERT-NOT-FOUND))
    )
    (asserts! (is-eq (get owner alert-data) caller) ERR-NOT-AUTHORIZED)
    
    ;; Deactivate (we don't actually delete to preserve history)
    (map-set alerts alert-id
      (merge alert-data { is-active: false })
    )
    
    (print {
      event: "alert-deleted",
      alert-id: alert-id,
      owner: caller,
      block: stacks-block-height
    })
    
    (ok true)
  )
)

;; Update fee vault (admin only)
(define-public (set-fee-vault (new-vault principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (var-set fee-vault-principal new-vault)
    (ok true)
  )
)

Functions (16)

FunctionAccessArgs
get-alertread-onlyalert-id: uint
get-user-alertsread-onlyuser: principal
get-user-alert-countread-onlyuser: principal
get-statsread-only
get-alert-type-statsread-onlyalert-type: uint
is-valid-alert-typeread-onlyalert-type: uint
get-monitored-contractread-onlycontract-principal: principal
update-alert-type-createdprivatealert-type: uint
update-alert-type-triggeredprivatealert-type: uint
create-alertpublicalert-type: uint, name: (string-ascii 64
toggle-alertpublicalert-id: uint
update-thresholdpublicalert-id: uint, new-threshold: uint
update-webhookpublicalert-id: uint, new-webhook: (optional (string-ascii 256
record-triggerpublicalert-id: uint
delete-alertpublicalert-id: uint
set-fee-vaultpublicnew-vault: principal