Source Code

(define-constant CONTRACT-OWNER tx-sender)

(define-constant MIN-MEMBERS u3)
(define-constant MAX-MEMBERS u50)
(define-constant MIN-CONTRIBUTION u10000)
(define-constant MAX-CONTRIBUTION u10000000)
(define-constant BLOCKS-PER-DAY u144)

(define-constant STATUS-PENDING u0)
(define-constant STATUS-FUNDING u1)
(define-constant STATUS-ACTIVE u2)
(define-constant STATUS-COMPLETED u3)
(define-constant STATUS-CANCELLED u4)

(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-CIRCLE-NOT-FULL (err u1003))
(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-DEPOSITS-INCOMPLETE (err u1011))
(define-constant ERR-PAYOUT-NOT-DUE (err u1012))
(define-constant ERR-ALREADY-CLAIMED (err u1013))
(define-constant ERR-NOT-YOUR-TURN (err u1014))
(define-constant ERR-CIRCLE-NOT-ACTIVE (err u1015))
(define-constant ERR-CIRCLE-COMPLETED (err u1016))
(define-constant ERR-PAUSED (err u1021))
(define-constant ERR-INVALID-SLOT (err u1022))

(define-map circles
  uint
  {
    creator: principal,
    name: (string-ascii 50),
    contribution: uint,
    max-members: uint,
    payout-interval: uint,
    status: uint,
    current-round: uint,
    start-block: uint,
    member-count: uint,
    created-at: uint
  }
)

(define-map circle-members
  { circle-id: uint, member: principal }
  { slot: uint, joined-at: uint }
)

(define-map slot-to-member
  { circle-id: uint, slot: uint }
  principal
)

(define-map member-circles
  principal
  (list 20 uint)
)

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

(define-data-var nft-minting-enabled bool true)

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


(define-public (create-circle 
    (name (string-ascii 50))
    (contribution uint) 
    (max-members uint) 
    (payout-interval-days uint))
  (let
    (
      (creator tx-sender)
      (circle-id (+ (var-get circle-counter) u1))
      (payout-interval-blocks (* payout-interval-days BLOCKS-PER-DAY))
    )
    (asserts! (not (contract-call? .stacksusu-admin-v4 is-paused)) ERR-PAUSED)
    (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)
    
    (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
      }
    )
    
    (var-set circle-counter circle-id)
    (try! (internal-join-circle circle-id creator))
    (ok 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)))
    )
    (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! (<= (get status circle) STATUS-FUNDING) ERR-CIRCLE-NOT-ACTIVE)
    
    (map-set circle-members 
      { circle-id: circle-id, member: member }
      { slot: slot, joined-at: block-height }
    )
    
    (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-CIRCLE-FULL)
    )
    
    (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-FUNDING 
                    STATUS-PENDING)
        })
      )
    )
    
    (ok slot)
  )
)

(define-public (join-circle (circle-id uint))
  (begin
    (asserts! (not (contract-call? .stacksusu-admin-v4 is-paused)) ERR-PAUSED)
    (internal-join-circle circle-id tx-sender)
  )
)


(define-public (deposit-to-circle (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: tx-sender }) 
                           ERR-NOT-MEMBER))
      (required-deposit (* (get contribution circle) (get max-members circle)))
    )
    (asserts! (is-eq (get status circle) STATUS-FUNDING) ERR-CIRCLE-NOT-ACTIVE)
    
    (try! (contract-call? .stacksusu-escrow-v4 deposit circle-id required-deposit true))
    
    (let
      (
        (deposit-status (unwrap-panic (contract-call? .stacksusu-escrow-v4 get-circle-deposit-status circle-id)))
      )
      (if (is-eq (get deposit-count deposit-status) (get max-members circle))
        (begin
          (map-set circles circle-id
            (merge circle {
              status: STATUS-ACTIVE,
              start-block: block-height
            })
          )
          (ok true)
        )
        (ok true)
      )
    )
  )
)


(define-public (claim-payout (circle-id uint))
  (let
    (
      (circle (unwrap! (map-get? circles circle-id) ERR-CIRCLE-NOT-FOUND))
      (caller tx-sender)
      (member-info (unwrap! (map-get? circle-members { circle-id: circle-id, member: caller }) 
                           ERR-NOT-MEMBER))
      (current-round (get current-round circle))
      (payout-interval (get payout-interval circle))
      (start-block (get start-block circle))
      (contribution (get contribution circle))
      (max-members (get max-members circle))
      (total-pot (* contribution max-members))
      (admin-fee-bps (contract-call? .stacksusu-admin-v4 get-admin-fee-bps))
      (admin-fee (/ (* total-pot admin-fee-bps) u10000))
    )
    (asserts! (is-eq (get status circle) STATUS-ACTIVE) ERR-CIRCLE-NOT-ACTIVE)
    (asserts! (is-eq (get slot member-info) current-round) ERR-NOT-YOUR-TURN)
    
    (let ((blocks-since-start (- block-height start-block)))
      (asserts! (>= blocks-since-start (* current-round payout-interval)) ERR-PAYOUT-NOT-DUE)
    )
    
    (asserts! (not (contract-call? .stacksusu-escrow-v4 has-received-payout circle-id caller)) 
              ERR-ALREADY-CLAIMED)
    
    (let ((payout-result (try! (contract-call? .stacksusu-escrow-v4 process-payout 
                                 circle-id current-round caller total-pot admin-fee))))
      (let ((next-round (+ current-round u1)))
        (if (>= next-round max-members)
          (map-set circles circle-id (merge circle { 
            current-round: next-round,
            status: STATUS-COMPLETED 
          }))
          (map-set circles circle-id (merge circle { current-round: next-round }))
        )
      )
      (ok payout-result)
    )
  )
)


(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-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) (get payout-interval c))))
      ERR-CIRCLE-NOT-FOUND
    )
  )
)

(define-read-only (get-required-deposit (circle-id uint))
  (let ((circle (map-get? circles circle-id)))
    (match circle
      c (ok (* (get contribution c) (get max-members c)))
      ERR-CIRCLE-NOT-FOUND
    )
  )
)

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


(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 }
                  { slot: slot, joined-at: (get joined-at info) }
                )
                (ok true)
              )
            (ok true)
          )
        )
      (ok true)
    )
  )
)

(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 (21)

FunctionAccessArgs
create-circlepublicname: (string-ascii 50
internal-join-circleprivatecircle-id: uint, member: principal
join-circlepubliccircle-id: uint
deposit-to-circlepubliccircle-id: uint
claim-payoutpubliccircle-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-next-payout-blockread-onlycircle-id: uint
get-required-depositread-onlycircle-id: uint
is-circle-readyread-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
set-nft-mintingpublicenabled: bool
is-nft-minting-enabledread-only