Source Code


;; title: recurbit
;; version: 1.0
;; summary: Automated Bitcoin Dollar Cost Averaging (DCA) service
;; description: Allows users to schedule and automate Bitcoin purchases on Stacks.

;; traits
;;

;; token definitions
;;

;; constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-found (err u101))
(define-constant err-already-exists (err u102))
(define-constant err-invalid-amount (err u103))
(define-constant err-insufficient-balance (err u104))
(define-constant err-unauthorized (err u105))
(define-constant err-plan-paused (err u106))
(define-constant err-too-early (err u107))

;; Fixed exchange rate for simulation: 1 STX = 1000 Sats (example)
;; In reality this would come from an oracle
(define-constant SIMULATED-BTC-PRICE-STX u50) ;; 50 STX per BTC (very cheap for testing!) or similar ratio

;; data vars
(define-data-var next-plan-id uint u1)
(define-data-var next-purchase-id uint u1)
(define-data-var counter uint u0) ;; Utility counter

;; data maps
(define-map dca-plans
    uint
    {
        owner: principal,
        frequency-blocks: uint, ;; simplifying "frequency" to blocks for MVP
        amount-per-purchase: uint,
        total-deposited: uint,
        total-spent: uint,
        bitcoin-acquired: uint,
        purchases-completed: uint,
        next-purchase-block: uint,
        status: (string-ascii 10), ;; "active", "paused", "cancelled"
        created-at: uint
    }
)

(define-map purchase-history
    uint
    {
        plan-id: uint,
        block-height: uint,
        stx-spent: uint,
        btc-acquired: uint,
        btc-price: uint,
        timestamp: uint
    }
)

(define-map user-stats
    principal
    {
        total-plans: uint,
        active-plans: uint,
        total-invested: uint,
        total-btc-acquired: uint
    }
)

;; public functions

;; Utility Counter Logic (Requested)
(define-public (count-up)
    (begin
        (var-set counter (+ (var-get counter) u1))
        (ok (var-get counter))
    )
)

(define-read-only (get-counter)
    (ok (var-get counter))
)

;; Core DCA Functions

(define-public (create-dca-plan (frequency-blocks uint) (amount uint) (start-delay uint))
    (let
        (
            (plan-id (var-get next-plan-id))
            (start-block (+ block-height start-delay))
        )
        (asserts! (> amount u0) err-invalid-amount)
        (asserts! (> frequency-blocks u0) err-invalid-amount)

        (map-set dca-plans plan-id
            {
                owner: tx-sender,
                frequency-blocks: frequency-blocks,
                amount-per-purchase: amount,
                total-deposited: u0,
                total-spent: u0,
                bitcoin-acquired: u0,
                purchases-completed: u0,
                next-purchase-block: start-block,
                status: "active",
                created-at: block-height
            }
        )
        
        ;; Update user stats
        (let
            ((stats (default-to { total-plans: u0, active-plans: u0, total-invested: u0, total-btc-acquired: u0 } (map-get? user-stats tx-sender))))
            (map-set user-stats tx-sender
                (merge stats {
                    total-plans: (+ (get total-plans stats) u1),
                    active-plans: (+ (get active-plans stats) u1)
                })
            )
        )

        (var-set next-plan-id (+ plan-id u1))
        (ok plan-id)
    )
)

(define-public (deposit-funds (plan-id uint) (amount uint))
    (let
        (
            (plan (unwrap! (map-get? dca-plans plan-id) err-not-found))
        )
        (asserts! (> amount u0) err-invalid-amount)
        ;; Transfer STX from user to contract
        (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
        
        ;; Update plan balance info
        (map-set dca-plans plan-id
            (merge plan {
                total-deposited: (+ (get total-deposited plan) amount)
            })
        )
        (ok true)
    )
)

(define-public (execute-purchase (plan-id uint))
    (let
        (
            (plan (unwrap! (map-get? dca-plans plan-id) err-not-found))
            (amount (get amount-per-purchase plan))
            (balance (- (get total-deposited plan) (get total-spent plan)))
            (is-active (is-eq (get status plan) "active"))
            (is-due (>= block-height (get next-purchase-block plan)))
        )
        (asserts! is-active err-plan-paused)
        (asserts! is-due err-too-early)
        (asserts! (>= balance amount) err-insufficient-balance)

        ;; Calculate BTC amount (Simulated exchange)
        ;; For simplicity, 1 STX = 1 MockBTC unit (adjusted by some factor if needed)
        ;; Let's say 1 STX buys 100 MockSatoshis
        (let
             (
                (btc-amount (* amount u100)) ;; Simulation rate
                (purchase-id (var-get next-purchase-id))
             )
             
             ;; Mint Mock BTC to the plan owner
             ;; We use contract-call? to the sibling contract
             (try! (contract-call? .mock-btc mint btc-amount (get owner plan)))

             ;; Record Purchase
             (map-set purchase-history purchase-id
                {
                    plan-id: plan-id,
                    block-height: block-height,
                    stx-spent: amount,
                    btc-acquired: btc-amount,
                    btc-price: u100, ;; simulated price
                    timestamp: u0 ;; clarity doesn't have native timestamp, usually rely on block-header or ignore
                }
             )

             ;; Update Plan
             (map-set dca-plans plan-id
                (merge plan {
                    total-spent: (+ (get total-spent plan) amount),
                    bitcoin-acquired: (+ (get bitcoin-acquired plan) btc-amount),
                    purchases-completed: (+ (get purchases-completed plan) u1),
                    next-purchase-block: (+ block-height (get frequency-blocks plan))
                })
             )

             (var-set next-purchase-id (+ purchase-id u1))
             (ok purchase-id)
        )
    )
)

;; read only functions

(define-read-only (get-plan (plan-id uint))
    (map-get? dca-plans plan-id)
)

(define-read-only (get-purchase-history (purchase-id uint))
    (map-get? purchase-history purchase-id)
)

(define-read-only (get-user-stats (user principal))
    (default-to { total-plans: u0, active-plans: u0, total-invested: u0, total-btc-acquired: u0 } (map-get? user-stats user))
)

Functions (8)

FunctionAccessArgs
count-uppublic
get-counterread-only
create-dca-planpublicfrequency-blocks: uint, amount: uint, start-delay: uint
deposit-fundspublicplan-id: uint, amount: uint
execute-purchasepublicplan-id: uint
get-planread-onlyplan-id: uint
get-purchase-historyread-onlypurchase-id: uint
get-user-statsread-onlyuser: principal