Source Code

;; StackSusu Core v6
;; Enhanced circle management with batch operations, delegation, and scheduling

(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)
(define-constant MAX-CIRCLES-PER-MEMBER u20)

;; 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
(define-constant MODE-ROUND-BY-ROUND u1) ;; Members contribute each round
(define-constant MODE-SCHEDULED u2)      ;; NEW: Auto-scheduled contributions

;; 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))
(define-constant ERR-BATCH-TOO-LARGE (err u1050))
(define-constant ERR-NOT-DELEGATED (err u1051))
(define-constant ERR-ALREADY-DELEGATED (err u1052))
(define-constant ERR-SCHEDULE-NOT-DUE (err u1053))

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

;; Circle data structure
(define-map circles
  uint
  {
    creator: principal,
    name: (string-ascii 50),
    description: (string-ascii 200),     ;; NEW: Circle description
    contribution: uint,
    max-members: uint,
    payout-interval: uint,
    status: uint,
    current-round: uint,
    start-block: uint,
    member-count: uint,
    created-at: uint,
    contribution-mode: uint,
    min-reputation: uint,
    total-contributed: uint,
    total-paid-out: uint,
    current-pot: uint,                   ;; Current pot balance
    is-private: bool,                    ;; NEW: Private circles require invite
    auto-start: bool                     ;; NEW: Auto-start when full
  }
)

;; Member data
(define-map circle-members
  { circle-id: uint, member: principal }
  { 
    slot: uint, 
    joined-at: uint,
    contributions-made: uint,
    last-contribution-round: uint,
    delegate: (optional principal),      ;; NEW: Delegation support
    auto-contribute: bool                ;; NEW: Auto-contribution flag
  }
)

;; Slot to member mapping
(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, delegated-by: (optional principal) }
)

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

;; Delegation mappings
(define-map contribution-delegates
  { circle-id: uint, member: principal }
  { delegate: principal, authorized-at: uint, max-amount: uint, last-contribution: uint }
)

;; Private circle invites
(define-map circle-invites
  { circle-id: uint, invitee: principal }
  { invited-by: principal, invited-at: uint, accepted: bool }
)

;; Scheduled contributions
(define-map scheduled-contributions
  { circle-id: uint, member: principal }
  { next-block: uint, amount: uint, enabled: bool }
)


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

(define-public (create-circle 
    (name (string-ascii 50))
    (description (string-ascii 200))
    (contribution uint)
    (max-members uint)
    (payout-interval uint)
    (contribution-mode uint)
    (min-reputation uint)
    (is-private bool)
    (auto-start bool))
  (let
    (
      (creator tx-sender)
      (circle-id (+ (var-get circle-counter) u1))
      (current-circles (default-to (list) (map-get? member-circles creator)))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v6 is-paused)) ERR-PAUSED)
    (asserts! (and (>= max-members MIN-MEMBERS) (<= max-members MAX-MEMBERS)) ERR-INVALID-MEMBERS)
    (asserts! (and (>= contribution MIN-CONTRIBUTION) (<= contribution MAX-CONTRIBUTION)) ERR-INVALID-AMOUNT)
    (asserts! (>= payout-interval BLOCKS-PER-DAY) ERR-INVALID-INTERVAL)
    (asserts! (<= contribution-mode MODE-SCHEDULED) ERR-INVALID-MODE)
    (asserts! (< (len current-circles) MAX-CIRCLES-PER-MEMBER) ERR-MAX-CIRCLES-REACHED)
    
    ;; Create circle
    (map-set circles circle-id {
      creator: creator,
      name: name,
      description: description,
      contribution: contribution,
      max-members: max-members,
      payout-interval: payout-interval,
      status: STATUS-PENDING,
      current-round: u1,
      start-block: u0,
      member-count: u1,
      created-at: block-height,
      contribution-mode: contribution-mode,
      min-reputation: min-reputation,
      total-contributed: u0,
      total-paid-out: u0,
      current-pot: u0,
      is-private: is-private,
      auto-start: auto-start
    })
    
    ;; Add creator as first member
    (map-set circle-members { circle-id: circle-id, member: creator }
      { slot: u1, joined-at: block-height, contributions-made: u0, 
        last-contribution-round: u0, delegate: none, auto-contribute: false })
    (map-set slot-to-member { circle-id: circle-id, slot: u1 } creator)
    
    ;; Track member's circles
    (map-set member-circles creator 
      (unwrap! (as-max-len? (append current-circles circle-id) u20) ERR-MAX-CIRCLES-REACHED))
    
    ;; Initialize reputation
    (try! (contract-call? .stacksusu-reputation-v6 initialize-member creator))
    
    ;; Update admin stats
    (try! (contract-call? .stacksusu-admin-v6 increment-circles))
    
    (var-set circle-counter circle-id)
    (ok circle-id)
  )
)


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

(define-public (join-circle (circle-id uint) (preferred-slot uint))
  (let
    (
      (member tx-sender)
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (current-circles (default-to (list) (map-get? member-circles member)))
      (member-count (get member-count circle))
      (next-slot (if (is-eq preferred-slot u0) (+ member-count u1) preferred-slot))
    )
    ;; Validations
    (asserts! (not (contract-call? .stacksusu-admin-v6 is-paused)) ERR-PAUSED)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-NOT-ACTIVE)
    (asserts! (< member-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! (< (len current-circles) MAX-CIRCLES-PER-MEMBER) ERR-MAX-CIRCLES-REACHED)
    
    ;; Check private circle invite
    (if (get is-private circle)
      (asserts! (is-invited circle-id member) ERR-NOT-AUTHORIZED)
      true
    )
    
    ;; Check reputation requirement
    (if (> (get min-reputation circle) u0)
      (asserts! (contract-call? .stacksusu-reputation-v6 meets-requirement member (get min-reputation circle))
                ERR-REPUTATION-TOO-LOW)
      true
    )
    
    ;; Validate slot
    (asserts! (and (> next-slot u0) (<= next-slot (get max-members circle))) ERR-INVALID-SLOT)
    (asserts! (is-none (map-get? slot-to-member { circle-id: circle-id, slot: next-slot })) ERR-INVALID-SLOT)
    
    ;; Add member
    (map-set circle-members { circle-id: circle-id, member: member }
      { slot: next-slot, joined-at: block-height, contributions-made: u0,
        last-contribution-round: u0, delegate: none, auto-contribute: false })
    (map-set slot-to-member { circle-id: circle-id, slot: next-slot } member)
    
    ;; Update circle
    (map-set circles circle-id (merge circle { member-count: (+ member-count u1) }))
    
    ;; Track member's circles
    (map-set member-circles member
      (unwrap! (as-max-len? (append current-circles circle-id) u20) ERR-MAX-CIRCLES-REACHED))
    
    ;; Initialize reputation
    (try! (contract-call? .stacksusu-reputation-v6 initialize-member member))
    
    ;; Mark invite as accepted if private
    (if (get is-private circle)
      (map-set circle-invites { circle-id: circle-id, invitee: member }
        { invited-by: (get creator circle), invited-at: block-height, accepted: true })
      true
    )
    
    ;; Auto-start if enabled and full
    (if (and (get auto-start circle) (is-eq (+ member-count u1) (get max-members circle)))
      (start-circle-internal circle-id)
      (ok true)
    )
  )
)


;; ============================================
;; Batch Join (NEW in v6)
;; ============================================

(define-public (batch-join-circle (circle-id uint) (members (list 10 principal)))
  (let
    (
      (caller tx-sender)
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (is-eq caller (get creator circle)) ERR-NOT-AUTHORIZED)
    (asserts! (<= (len members) u10) ERR-BATCH-TOO-LARGE)
    (asserts! (is-eq (get status circle) STATUS-PENDING) ERR-CIRCLE-NOT-ACTIVE)
    
    ;; Send invites to all members
    (map add-batch-invite (list circle-id circle-id circle-id circle-id circle-id 
                                 circle-id circle-id circle-id circle-id circle-id)
         members
         (list caller caller caller caller caller caller caller caller caller caller))
    
    (ok true)
  )
)

(define-private (add-batch-invite (circle-id uint) (invitee principal) (inviter principal))
  (map-set circle-invites { circle-id: circle-id, invitee: invitee }
    { invited-by: inviter, invited-at: block-height, accepted: false })
)


;; ============================================
;; Delegation Functions (NEW in v6)
;; ============================================

(define-public (delegate-contributions (circle-id uint) (delegate principal) (max-amount uint))
  (let
    (
      (member tx-sender)
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
    )
    (asserts! (is-none (get delegate member-info)) ERR-ALREADY-DELEGATED)
    (asserts! (not (is-eq member delegate)) ERR-NOT-AUTHORIZED)
    
    (map-set contribution-delegates { circle-id: circle-id, member: member }
      { delegate: delegate, authorized-at: block-height, max-amount: max-amount, last-contribution: u0 })
    
    (map-set circle-members { circle-id: circle-id, member: member }
      (merge member-info { delegate: (some delegate) }))
    
    (ok true)
  )
)

(define-public (revoke-delegation (circle-id uint))
  (let
    (
      (member tx-sender)
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
    )
    (map-delete contribution-delegates { circle-id: circle-id, member: member })
    (map-set circle-members { circle-id: circle-id, member: member }
      (merge member-info { delegate: none }))
    (ok true)
  )
)

(define-public (contribute-as-delegate (circle-id uint) (member principal) (amount uint))
  (let
    (
      (delegate tx-sender)
      (delegation (unwrap! (map-get? contribution-delegates { circle-id: circle-id, member: member }) ERR-NOT-DELEGATED))
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (asserts! (is-eq delegate (get delegate delegation)) ERR-NOT-DELEGATED)
    (asserts! (<= amount (get max-amount delegation)) ERR-INVALID-AMOUNT)
    
    ;; Mark delegation as used - actual payment handled by escrow contract
    (map-set contribution-delegates { circle-id: circle-id, member: member }
      (merge delegation { last-contribution: block-height })
    )
    
    (ok { circle-id: circle-id, member: member, delegate: delegate, amount: amount })
  )
)


;; ============================================
;; Scheduled Contributions (NEW in v6)
;; ============================================

(define-public (setup-auto-contribute (circle-id uint) (amount uint))
  (let
    (
      (member tx-sender)
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (map-set scheduled-contributions { circle-id: circle-id, member: member }
      { next-block: (+ block-height (get payout-interval circle)), amount: amount, enabled: true })
    
    (map-set circle-members { circle-id: circle-id, member: member }
      (merge member-info { auto-contribute: true }))
    
    (ok true)
  )
)

(define-public (disable-auto-contribute (circle-id uint))
  (let
    (
      (member tx-sender)
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: member }) ERR-NOT-MEMBER))
    )
    (map-delete scheduled-contributions { circle-id: circle-id, member: member })
    (map-set circle-members { circle-id: circle-id, member: member }
      (merge member-info { auto-contribute: false }))
    (ok true)
  )
)


;; ============================================
;; Circle Management
;; ============================================

(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! (is-eq (get member-count circle) (get max-members circle)) ERR-CIRCLE-NOT-READY)
    
    (start-circle-internal circle-id)
  )
)

(define-private (start-circle-internal (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
    )
    (map-set circles circle-id (merge circle {
      status: STATUS-ACTIVE,
      start-block: block-height
    }))
    
    ;; Initialize first round
    (map-set round-status { circle-id: circle-id, round: u1 }
      { contributions-received: u0, total-amount: u0, payout-processed: false, 
        recipient: none, started-at: block-height })
    
    (ok true)
  )
)

(define-public (update-slot-holder (circle-id uint) (slot uint) (new-holder principal))
  (begin
    (asserts! (contract-call? .stacksusu-admin-v6 is-authorized-contract contract-caller) ERR-NOT-AUTHORIZED)
    (map-set slot-to-member { circle-id: circle-id, slot: slot } new-holder)
    (ok true)
  )
)


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

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

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

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

(define-read-only (get-member-slot (circle-id uint) (member principal))
  (match (map-get? circle-members { circle-id: circle-id, member: member })
    member-data (ok (get slot member-data))
    ERR-NOT-MEMBER
  )
)

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

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

(define-read-only (is-invited (circle-id uint) (member principal))
  (match (map-get? circle-invites { circle-id: circle-id, invitee: member })
    invite (not (get accepted invite))
    false
  )
)

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

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

(define-read-only (get-delegation (circle-id uint) (member principal))
  (map-get? contribution-delegates { circle-id: circle-id, member: member })
)

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

Functions (23)

FunctionAccessArgs
create-circlepublicname: (string-ascii 50
join-circlepubliccircle-id: uint, preferred-slot: uint
batch-join-circlepubliccircle-id: uint, members: (list 10 principal
add-batch-inviteprivatecircle-id: uint, invitee: principal, inviter: principal
delegate-contributionspubliccircle-id: uint, delegate: principal, max-amount: uint
revoke-delegationpubliccircle-id: uint
contribute-as-delegatepubliccircle-id: uint, member: principal, amount: uint
setup-auto-contributepubliccircle-id: uint, amount: uint
disable-auto-contributepubliccircle-id: uint
start-circlepubliccircle-id: uint
start-circle-internalprivatecircle-id: uint
update-slot-holderpubliccircle-id: uint, slot: uint, new-holder: principal
get-circle-inforead-onlycircle-id: uint
is-memberread-onlycircle-id: uint, member: principal
get-member-inforead-onlycircle-id: uint, member: principal
get-member-slotread-onlycircle-id: uint, member: principal
get-slot-holderread-onlycircle-id: uint, slot: uint
get-member-circlesread-onlymember: principal
is-invitedread-onlycircle-id: uint, member: principal
get-circle-countread-only
get-round-statusread-onlycircle-id: uint, round: uint
get-delegationread-onlycircle-id: uint, member: principal
get-scheduled-contributionread-onlycircle-id: uint, member: principal