Source Code

;; StackSusu Core v5
;; Enhanced circle management with round-by-round contributions

(define-constant CONTRACT-OWNER tx-sender)

;; Circle size limits
(define-constant MIN-MEMBERS u3)
(define-constant MAX-MEMBERS u50)
(define-constant MIN-CONTRIBUTION u10000)        ;; 0.01 STX minimum
(define-constant MAX-CONTRIBUTION u100000000)    ;; 100 STX maximum per round
(define-constant BLOCKS-PER-DAY u144)

;; Circle status constants
(define-constant STATUS-PENDING u0)      ;; Waiting for members
(define-constant STATUS-ACTIVE u1)       ;; Circle is running rounds
(define-constant STATUS-COMPLETED u2)    ;; All rounds finished
(define-constant STATUS-CANCELLED u3)    ;; Circle was cancelled
(define-constant STATUS-PAUSED u4)       ;; Temporarily paused

;; Contribution mode constants
(define-constant MODE-UPFRONT u0)        ;; All members deposit full amount upfront (v4 style)
(define-constant MODE-ROUND-BY-ROUND u1) ;; Members contribute each round

;; Error constants
(define-constant ERR-NOT-AUTHORIZED (err u1000))
(define-constant ERR-CIRCLE-NOT-FOUND (err u1001))
(define-constant ERR-CIRCLE-FULL (err u1002))
(define-constant ERR-ALREADY-MEMBER (err u1004))
(define-constant ERR-NOT-MEMBER (err u1005))
(define-constant ERR-INVALID-AMOUNT (err u1006))
(define-constant ERR-INVALID-MEMBERS (err u1007))
(define-constant ERR-INVALID-INTERVAL (err u1008))
(define-constant ERR-CIRCLE-NOT-ACTIVE (err u1015))
(define-constant ERR-PAUSED (err u1021))
(define-constant ERR-INVALID-SLOT (err u1022))
(define-constant ERR-MAX-CIRCLES-REACHED (err u1026))
(define-constant ERR-REPUTATION-TOO-LOW (err u1027))
(define-constant ERR-CIRCLE-NOT-READY (err u1028))
(define-constant ERR-INVALID-MODE (err u1029))

;; Circle data structure
(define-map circles
  uint
  {
    creator: principal,
    name: (string-ascii 50),
    contribution: uint,           ;; Amount per member per round
    max-members: uint,
    payout-interval: uint,        ;; Blocks between payouts
    status: uint,
    current-round: uint,
    start-block: uint,
    member-count: uint,
    created-at: uint,
    contribution-mode: uint,      ;; NEW: upfront or round-by-round
    min-reputation: uint,         ;; NEW: minimum reputation required
    total-contributed: uint,      ;; NEW: total STX contributed to circle
    total-paid-out: uint          ;; NEW: total STX paid out
  }
)

;; Member data
(define-map circle-members
  { circle-id: uint, member: principal }
  { 
    slot: uint, 
    joined-at: uint,
    contributions-made: uint,     ;; NEW: count of contributions
    last-contribution-round: uint ;; NEW: last round contributed to
  }
)

;; Slot to member mapping (for payout order)
(define-map slot-to-member
  { circle-id: uint, slot: uint }
  principal
)

;; Member's circles list
(define-map member-circles
  principal
  (list 20 uint)
)

;; Round contribution tracking
(define-map round-contributions
  { circle-id: uint, round: uint, member: principal }
  { amount: uint, contributed-at: uint, is-late: bool }
)

;; Round status
(define-map round-status
  { circle-id: uint, round: uint }
  {
    contributions-received: uint,
    total-amount: uint,
    payout-processed: bool,
    recipient: (optional principal),
    started-at: uint
  }
)

;; Counters
(define-data-var circle-counter uint u0)

;; NFT minting setting
(define-data-var nft-minting-enabled bool true)

;; Authorized slot updaters
(define-map authorized-slot-updaters principal bool)


;; ============================================
;; Circle Creation
;; ============================================

(define-public (create-circle 
    (name (string-ascii 50))
    (contribution uint) 
    (max-members uint) 
    (payout-interval-days uint)
    (contribution-mode uint)
    (min-reputation uint))
  (let
    (
      (creator tx-sender)
      (circle-id (+ (var-get circle-counter) u1))
      (payout-interval-blocks (* payout-interval-days BLOCKS-PER-DAY))
      (creator-circles (default-to (list) (map-get? member-circles creator)))
      (max-allowed (contract-call? .stacksusu-admin-v5 get-max-circles-per-member))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v5 is-paused)) ERR-PAUSED)
    (asserts! (< (len creator-circles) max-allowed) ERR-MAX-CIRCLES-REACHED)
    (asserts! (and (>= contribution MIN-CONTRIBUTION) (<= contribution MAX-CONTRIBUTION)) 
              ERR-INVALID-AMOUNT)
    (asserts! (and (>= max-members MIN-MEMBERS) (<= max-members MAX-MEMBERS)) 
              ERR-INVALID-MEMBERS)
    (asserts! (and (>= payout-interval-days u1) (<= payout-interval-days u30)) 
              ERR-INVALID-INTERVAL)
    (asserts! (or (is-eq contribution-mode MODE-UPFRONT) 
                  (is-eq contribution-mode MODE-ROUND-BY-ROUND))
              ERR-INVALID-MODE)
    
    ;; Check creator reputation if required
    (if (contract-call? .stacksusu-admin-v5 is-reputation-required)
      (asserts! (contract-call? .stacksusu-reputation-v5 meets-requirement creator min-reputation)
                ERR-REPUTATION-TOO-LOW)
      true
    )
    
    ;; Create circle
    (map-set circles circle-id
      {
        creator: creator,
        name: name,
        contribution: contribution,
        max-members: max-members,
        payout-interval: payout-interval-blocks,
        status: STATUS-PENDING,
        current-round: u0,
        start-block: u0,
        member-count: u0,
        created-at: block-height,
        contribution-mode: contribution-mode,
        min-reputation: min-reputation,
        total-contributed: u0,
        total-paid-out: u0
      }
    )
    
    ;; Initialize first round status
    (map-set round-status { circle-id: circle-id, round: u0 }
      {
        contributions-received: u0,
        total-amount: u0,
        payout-processed: false,
        recipient: none,
        started-at: block-height
      }
    )
    
    (var-set circle-counter circle-id)
    
    ;; Creator auto-joins
    (try! (internal-join-circle circle-id creator))
    
    ;; Initialize reputation for creator
    (try! (contract-call? .stacksusu-reputation-v5 initialize-member creator))
    
    ;; Record in admin stats
    (try! (contract-call? .stacksusu-admin-v5 increment-circles-created))
    
    (ok circle-id)
  )
)

;; Legacy create-circle (for backwards compatibility)
(define-public (create-circle-simple
    (name (string-ascii 50))
    (contribution uint) 
    (max-members uint) 
    (payout-interval-days uint))
  (create-circle name contribution max-members payout-interval-days MODE-UPFRONT u0)
)


;; ============================================
;; Join Circle
;; ============================================

(define-public (join-circle (circle-id uint))
  (let
    (
      (joiner tx-sender)
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (not (contract-call? .stacksusu-admin-v5 is-paused)) ERR-PAUSED)
    
    ;; Check reputation requirement
    (if (> (get min-reputation circle) u0)
      (asserts! (contract-call? .stacksusu-reputation-v5 meets-requirement joiner (get min-reputation circle))
                ERR-REPUTATION-TOO-LOW)
      true
    )
    
    ;; Initialize reputation
    (try! (contract-call? .stacksusu-reputation-v5 initialize-member joiner))
    
    (internal-join-circle circle-id joiner)
  )
)

;; Join with referral
(define-public (join-circle-with-referral (circle-id uint) (referrer principal))
  (begin
    ;; Register referral first (will fail silently if already referred)
    (match (contract-call? .stacksusu-referral-v5 register-referral referrer)
      success true
      error true
    )
    ;; Then join circle
    (join-circle circle-id)
  )
)

(define-private (internal-join-circle (circle-id uint) (member principal))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (current-count (get member-count circle))
      (slot current-count)
      (member-circle-list (default-to (list) (map-get? member-circles member)))
      (max-allowed (contract-call? .stacksusu-admin-v5 get-max-circles-per-member))
    )
    ;; Validations
    (asserts! (< (len member-circle-list) max-allowed) ERR-MAX-CIRCLES-REACHED)
    (asserts! (< current-count (get max-members circle)) ERR-CIRCLE-FULL)
    (asserts! (is-none (map-get? circle-members { circle-id: circle-id, member: member })) 
              ERR-ALREADY-MEMBER)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-NOT-ACTIVE)
    
    ;; Add member
    (map-set circle-members 
      { circle-id: circle-id, member: member }
      { 
        slot: slot, 
        joined-at: block-height,
        contributions-made: u0,
        last-contribution-round: u0
      }
    )
    
    (map-set slot-to-member
      { circle-id: circle-id, slot: slot }
      member
    )
    
    (map-set member-circles member 
      (unwrap! (as-max-len? (append member-circle-list circle-id) u20) ERR-MAX-CIRCLES-REACHED)
    )
    
    ;; Update circle status
    (let ((new-count (+ current-count u1)))
      (map-set circles circle-id
        (merge circle { 
          member-count: new-count,
          status: (if (is-eq new-count (get max-members circle)) 
                    STATUS-ACTIVE 
                    STATUS-PENDING),
          start-block: (if (is-eq new-count (get max-members circle))
                         block-height
                         u0)
        })
      )
      
      ;; If circle is now full and active, update round start
      (if (is-eq new-count (get max-members circle))
        (map-set round-status { circle-id: circle-id, round: u0 }
          {
            contributions-received: u0,
            total-amount: u0,
            payout-processed: false,
            recipient: (map-get? slot-to-member { circle-id: circle-id, slot: u0 }),
            started-at: block-height
          }
        )
        true
      )
    )
    
    (ok slot)
  )
)


;; ============================================
;; Start Circle (manual start option)
;; ============================================

(define-public (start-circle (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (is-eq tx-sender (get creator circle)) ERR-NOT-AUTHORIZED)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-NOT-ACTIVE)
    (asserts! (>= (get member-count circle) MIN-MEMBERS) ERR-CIRCLE-NOT-READY)
    
    ;; Activate circle
    (map-set circles circle-id
      (merge circle {
        status: STATUS-ACTIVE,
        start-block: block-height,
        max-members: (get member-count circle) ;; Lock in current member count
      })
    )
    
    ;; Initialize first round
    (map-set round-status { circle-id: circle-id, round: u0 }
      {
        contributions-received: u0,
        total-amount: u0,
        payout-processed: false,
        recipient: (map-get? slot-to-member { circle-id: circle-id, slot: u0 }),
        started-at: block-height
      }
    )
    
    (ok true)
  )
)


;; ============================================
;; Read-only Functions
;; ============================================

(define-read-only (get-circle-info (circle-id uint))
  (ok (map-get? circles circle-id))
)

(define-read-only (get-circle-count)
  (var-get circle-counter)
)

(define-read-only (get-member-info (circle-id uint) (member principal))
  (ok (map-get? circle-members { circle-id: circle-id, member: member }))
)

(define-read-only (get-member-at-slot (circle-id uint) (slot uint))
  (ok (map-get? slot-to-member { circle-id: circle-id, slot: slot }))
)

(define-read-only (get-my-circles (member principal))
  (ok (default-to (list) (map-get? member-circles member)))
)

(define-read-only (is-member (circle-id uint) (addr principal))
  (is-some (map-get? circle-members { circle-id: circle-id, member: addr }))
)

(define-read-only (get-current-recipient (circle-id uint))
  (let ((circle (map-get? circles circle-id)))
    (match circle
      c (map-get? slot-to-member { circle-id: circle-id, slot: (get current-round c) })
      none
    )
  )
)

(define-read-only (get-round-status (circle-id uint) (round uint))
  (ok (map-get? round-status { circle-id: circle-id, round: round }))
)

(define-read-only (get-round-contribution (circle-id uint) (round uint) (member principal))
  (ok (map-get? round-contributions { circle-id: circle-id, round: round, member: member }))
)

(define-read-only (get-next-payout-block (circle-id uint))
  (let ((circle (map-get? circles circle-id)))
    (match circle
      c (ok (+ (get start-block c) (* (+ (get current-round c) u1) (get payout-interval c))))
      ERR-CIRCLE-NOT-FOUND
    )
  )
)

(define-read-only (get-required-contribution (circle-id uint))
  (let ((circle (map-get? circles circle-id)))
    (match circle
      c (if (is-eq (get contribution-mode c) MODE-UPFRONT)
          (ok (* (get contribution c) (get max-members c)))  ;; Full amount upfront
          (ok (get contribution c)))                          ;; Just one round
      ERR-CIRCLE-NOT-FOUND
    )
  )
)

(define-read-only (is-circle-ready (circle-id uint))
  (let ((circle (map-get? circles circle-id)))
    (match circle
      c (is-eq (get status c) STATUS-ACTIVE)
      false
    )
  )
)

(define-read-only (get-contribution-mode (circle-id uint))
  (match (map-get? circles circle-id)
    c (ok (get contribution-mode c))
    ERR-CIRCLE-NOT-FOUND
  )
)


;; ============================================
;; Slot Management (for NFT transfers)
;; ============================================

(define-public (authorize-slot-updater (updater principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (map-set authorized-slot-updaters updater true))
  )
)

(define-public (revoke-slot-updater (updater principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (map-delete authorized-slot-updaters updater))
  )
)

(define-read-only (is-authorized-slot-updater (caller principal))
  (or (is-eq caller CONTRACT-OWNER) (default-to false (map-get? authorized-slot-updaters caller)))
)

(define-public (update-slot-holder (circle-id uint) (slot uint) (new-holder principal))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (current-member (map-get? slot-to-member { circle-id: circle-id, slot: slot }))
    )
    (asserts! (is-authorized-slot-updater contract-caller) ERR-NOT-AUTHORIZED)
    (asserts! (< slot (get max-members circle)) ERR-INVALID-SLOT)
    
    (map-set slot-to-member { circle-id: circle-id, slot: slot } new-holder)
    
    (match current-member
      old-holder
        (let ((old-member-info (map-get? circle-members { circle-id: circle-id, member: old-holder })))
          (match old-member-info
            info
              (begin
                (map-delete circle-members { circle-id: circle-id, member: old-holder })
                (map-set circle-members 
                  { circle-id: circle-id, member: new-holder }
                  (merge info { slot: slot })
                )
                (ok true)
              )
            (ok true)
          )
        )
      (ok true)
    )
  )
)

;; Update circle stats (called by escrow)
(define-public (update-circle-stats (circle-id uint) (contributed uint) (paid-out uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (is-authorized-slot-updater contract-caller) ERR-NOT-AUTHORIZED)
    
    (map-set circles circle-id
      (merge circle {
        total-contributed: (+ (get total-contributed circle) contributed),
        total-paid-out: (+ (get total-paid-out circle) paid-out)
      })
    )
    (ok true)
  )
)

;; Advance to next round (called by escrow after payout)
(define-public (advance-round (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (next-round (+ (get current-round circle) u1))
    )
    (asserts! (is-authorized-slot-updater contract-caller) ERR-NOT-AUTHORIZED)
    
    (if (>= next-round (get max-members circle))
      ;; Circle completed
      (map-set circles circle-id (merge circle { 
        current-round: next-round,
        status: STATUS-COMPLETED 
      }))
      ;; Move to next round
      (begin
        (map-set circles circle-id (merge circle { current-round: next-round }))
        ;; Initialize next round
        (map-set round-status { circle-id: circle-id, round: next-round }
          {
            contributions-received: u0,
            total-amount: u0,
            payout-processed: false,
            recipient: (map-get? slot-to-member { circle-id: circle-id, slot: next-round }),
            started-at: block-height
          }
        )
      )
    )
    (ok next-round)
  )
)


;; ============================================
;; NFT Minting Control
;; ============================================

(define-public (set-nft-minting (enabled bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (var-set nft-minting-enabled enabled))
  )
)

(define-read-only (is-nft-minting-enabled)
  (var-get nft-minting-enabled)
)

Functions (27)

FunctionAccessArgs
create-circlepublicname: (string-ascii 50
create-circle-simplepublicname: (string-ascii 50
join-circlepubliccircle-id: uint
join-circle-with-referralpubliccircle-id: uint, referrer: principal
internal-join-circleprivatecircle-id: uint, member: principal
start-circlepubliccircle-id: uint
get-circle-inforead-onlycircle-id: uint
get-circle-countread-only
get-member-inforead-onlycircle-id: uint, member: principal
get-member-at-slotread-onlycircle-id: uint, slot: uint
get-my-circlesread-onlymember: principal
is-memberread-onlycircle-id: uint, addr: principal
get-current-recipientread-onlycircle-id: uint
get-round-statusread-onlycircle-id: uint, round: uint
get-round-contributionread-onlycircle-id: uint, round: uint, member: principal
get-next-payout-blockread-onlycircle-id: uint
get-required-contributionread-onlycircle-id: uint
is-circle-readyread-onlycircle-id: uint
get-contribution-moderead-onlycircle-id: uint
authorize-slot-updaterpublicupdater: principal
revoke-slot-updaterpublicupdater: principal
is-authorized-slot-updaterread-onlycaller: principal
update-slot-holderpubliccircle-id: uint, slot: uint, new-holder: principal
update-circle-statspubliccircle-id: uint, contributed: uint, paid-out: uint
advance-roundpubliccircle-id: uint
set-nft-mintingpublicenabled: bool
is-nft-minting-enabledread-only