Source Code


;; SwiftPay Engine - Robust Payment Streaming
;; Supports STX and SIP-010 tokens with NFT-based ownership

(use-trait sip-010 .sip-010-trait.sip-010-trait)

;; Error Codes
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-INVALID-PARAMS (err u101))
(define-constant ERR-STREAM-NOT-FOUND (err u102))
(define-constant ERR-STREAM-CANCELLED (err u103))
(define-constant ERR-ALREADY-WITHDRAWN (err u104))
(define-constant ERR-PAUSED (err u105))

;; Constants
(define-constant CONTRACT-OWNER tx-sender)

;; Data Vars
(define-data-var next-stream-id uint u0)
(define-data-var is-paused bool false)
(define-data-var protocol-fee-percent uint u1) ;; 1% fee

;; Data Maps
(define-map streams 
    uint 
    {
        sender: principal,
        token-contract: (optional principal), ;; none for STX
        amount-total: uint,
        amount-withdrawn: uint,
        start-block: uint,
        stop-block: uint,
        is-cancelled: bool
    }
)

;; Authorization checks
(define-private (is-owner)
    (is-eq tx-sender CONTRACT-OWNER)
)

;; Read-Only Functions

(define-read-only (get-stream (stream-id uint))
    (map-get? streams stream-id)
)

(define-read-only (calculate-earned (stream-id uint))
    (let (
        (stream (unwrap! (map-get? streams stream-id) ERR-STREAM-NOT-FOUND))
        (current-height block-height)
    )
    (if (<= current-height (get start-block stream))
        (ok u0)
        (if (>= current-height (get stop-block stream))
            (ok (get amount-total stream))
            (let (
                (duration (- (get stop-block stream) (get start-block stream)))
                (elapsed (- current-height (get start-block stream)))
                ;; Using multiplication before division for precision
                (earned (/ (* (get amount-total stream) elapsed) duration))
            )
            (ok earned))
        )
    )
    )
)

;; Get the current recipient of a stream (from the NFT)
(define-read-only (get-recipient (stream-id uint))
    (contract-call? .swift-pay-nft get-owner stream-id)
)

;; Public Functions

;; Admin/Owner functions
(define-public (set-paused (paused bool))
    (begin
        (asserts! (is-owner) ERR-NOT-AUTHORIZED)
        (ok (var-set is-paused paused))
    )
)

;; Create STX stream
(define-public (create-stx-stream (recipient principal) (amount uint) (start-block uint) (stop-block uint))
    (let (
        (stream-id (var-get next-stream-id))
        (contract-addr (as-contract tx-sender))
    )
        (asserts! (not (var-get is-paused)) ERR-PAUSED)
        (asserts! (> amount u0) ERR-INVALID-PARAMS)
        (asserts! (> stop-block start-block) ERR-INVALID-PARAMS)
        (asserts! (>= start-block block-height) ERR-INVALID-PARAMS)

        ;; Transfer STX to contract
        (try! (stx-transfer? amount tx-sender contract-addr))

        ;; Record stream
        (map-set streams stream-id {
            sender: tx-sender,
            token-contract: none,
            amount-total: amount,
            amount-withdrawn: u0,
            start-block: start-block,
            stop-block: stop-block,
            is-cancelled: false
        })

        ;; Mint NFT to recipient
        (try! (contract-call? .swift-pay-nft mint recipient stream-id))

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

;; Create SIP-010 stream
(define-public (create-ft-stream (token <sip-010>) (recipient principal) (amount uint) (start-block uint) (stop-block uint))
    (let (
        (stream-id (var-get next-stream-id))
        (contract-addr (as-contract tx-sender))
        (token-addr (contract-of token))
    )
        (asserts! (not (var-get is-paused)) ERR-PAUSED)
        (asserts! (> amount u0) ERR-INVALID-PARAMS)
        (asserts! (> stop-block start-block) ERR-INVALID-PARAMS)
        (asserts! (>= start-block block-height) ERR-INVALID-PARAMS)

        ;; Transfer tokens to contract
        (try! (contract-call? token transfer amount tx-sender contract-addr none))

        ;; Record stream
        (map-set streams stream-id {
            sender: tx-sender,
            token-contract: (some token-addr),
            amount-total: amount,
            amount-withdrawn: u0,
            start-block: start-block,
            stop-block: stop-block,
            is-cancelled: false
        })

        ;; Mint NFT to recipient
        (try! (contract-call? .swift-pay-nft mint recipient stream-id))

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

;; Withdraw from stream
(define-public (withdraw (stream-id uint) (token-opt (optional <sip-010>)))
    (let (
        (stream (unwrap! (map-get? streams stream-id) ERR-STREAM-NOT-FOUND))
        (recipient (unwrap! (unwrap! (get-recipient stream-id) ERR-STREAM-NOT-FOUND) ERR-NOT-AUTHORIZED))
        (earned (unwrap! (calculate-earned stream-id) ERR-STREAM-NOT-FOUND))
        (withdrawable (- earned (get amount-withdrawn stream)))
    )
        (asserts! (not (get is-cancelled stream)) ERR-STREAM-CANCELLED)
        (asserts! (is-eq tx-sender recipient) ERR-NOT-AUTHORIZED)
        (asserts! (> withdrawable u0) ERR-ALREADY-WITHDRAWN)

        ;; Update stream
        (map-set streams stream-id (merge stream { amount-withdrawn: earned }))

        ;; Payout
        (match (get token-contract stream)
            t-addr (let ((token (unwrap! token-opt ERR-INVALID-PARAMS)))
                (asserts! (is-eq (contract-of token) t-addr) ERR-INVALID-PARAMS)
                (try! (as-contract (contract-call? token transfer withdrawable tx-sender recipient none)))
            )
            (try! (as-contract (stx-transfer? withdrawable tx-sender recipient)))
        )
        (ok withdrawable)
    )
)

;; Cancel stream
(define-public (cancel (stream-id uint) (token-opt (optional <sip-010>)))
    (let (
        (stream (unwrap! (map-get? streams stream-id) ERR-STREAM-NOT-FOUND))
        (sender (get sender stream))
        (recipient (unwrap! (unwrap! (get-recipient stream-id) ERR-STREAM-NOT-FOUND) ERR-NOT-AUTHORIZED))
        (earned (unwrap! (calculate-earned stream-id) ERR-STREAM-NOT-FOUND))
        (remaining (- (get amount-total stream) earned))
        (to-recipient (- earned (get amount-withdrawn stream)))
    )
        (asserts! (or (is-eq tx-sender sender) (is-eq tx-sender recipient)) ERR-NOT-AUTHORIZED)
        (asserts! (not (get is-cancelled stream)) ERR-STREAM-CANCELLED)

        ;; Update stream
        (map-set streams stream-id (merge stream { 
            is-cancelled: true,
            amount-withdrawn: earned 
        }))

        ;; Final payouts
        (match (get token-contract stream)
            t-addr (let ((token (unwrap! token-opt ERR-INVALID-PARAMS)))
                (asserts! (is-eq (contract-of token) t-addr) ERR-INVALID-PARAMS)
                (if (> to-recipient u0) 
                    (try! (as-contract (contract-call? token transfer to-recipient tx-sender recipient none)))
                    true
                )
                (if (> remaining u0)
                    (try! (as-contract (contract-call? token transfer remaining tx-sender sender none)))
                    true
                )
            )
            (begin
                (if (> to-recipient u0) 
                    (try! (as-contract (stx-transfer? to-recipient tx-sender recipient)))
                    true
                )
                (if (> remaining u0)
                    (try! (as-contract (stx-transfer? remaining tx-sender sender)))
                    true
                )
            )
        )

        ;; Burn NFT
        (try! (contract-call? .swift-pay-nft burn stream-id))

        (ok true)
    )
)
;; Final polish

Functions (9)

FunctionAccessArgs
is-ownerprivate
get-streamread-onlystream-id: uint
calculate-earnedread-onlystream-id: uint
get-recipientread-onlystream-id: uint
set-pausedpublicpaused: bool
create-stx-streampublicrecipient: principal, amount: uint, start-block: uint, stop-block: uint
create-ft-streampublictoken: <sip-010>, recipient: principal, amount: uint, start-block: uint, stop-block: uint
withdrawpublicstream-id: uint, token-opt: (optional <sip-010>
cancelpublicstream-id: uint, token-opt: (optional <sip-010>