Source Code

;; Title: BlockPay - Decentralized Payroll Streaming
;; Version: 2.0.0
;; Summary: Main payroll contract for real-time salary streaming
;; Description: Enables employers to create salary streams with vesting, employees to withdraw earned wages

;; ============================================
;; Traits
;; ============================================
(use-trait sip-010-trait .sip-010-trait.sip-010-trait)

;; ============================================
;; Constants - Error Codes
;; ============================================
(define-constant ERR_UNAUTHORIZED (err u5000))
(define-constant ERR_STREAM_NOT_FOUND (err u5001))
(define-constant ERR_STREAM_INACTIVE (err u5002))
(define-constant ERR_INSUFFICIENT_FUNDS (err u5003))
(define-constant ERR_INVALID_AMOUNT (err u5004))
(define-constant ERR_INVALID_DURATION (err u5005))
(define-constant ERR_INVALID_EMPLOYEE (err u5006))
(define-constant ERR_STREAM_PAUSED (err u5007))
(define-constant ERR_STREAM_COMPLETED (err u5008))
(define-constant ERR_STREAM_CANCELLED (err u5009))
(define-constant ERR_NOTHING_TO_WITHDRAW (err u5010))
(define-constant ERR_MODIFICATION_NOT_ALLOWED (err u5011))
(define-constant ERR_SYSTEM_PAUSED (err u5012))
(define-constant ERR_CLIFF_NOT_REACHED (err u5013))
(define-constant ERR_INVALID_VESTING_TYPE (err u5014))
(define-constant ERR_TOO_MANY_STREAMS (err u5015))

;; ============================================
;; Constants - Configuration
;; ============================================
(define-constant MAX_STREAMS_PER_EMPLOYER u200)
(define-constant MAX_STREAMS_PER_EMPLOYEE u200)
(define-constant STATUS_ACTIVE "active")
(define-constant STATUS_PAUSED "paused")
(define-constant STATUS_CANCELLED "cancelled")
(define-constant STATUS_COMPLETED "completed")
(define-constant VESTING_LINEAR "linear")
(define-constant VESTING_CLIFF_LINEAR "cliff-linear")

;; ============================================
;; Data Variables
;; ============================================
(define-data-var next-stream-id uint u1)
(define-data-var total-streams-created uint u0)
(define-data-var total-amount-streamed uint u0)

;; ============================================
;; Data Maps - Stream Data
;; ============================================
(define-map streams
    { stream-id: uint }
    {
        employer: principal,
        employee: principal,
        token-contract: (optional principal),
        total-amount: uint,
        rate-per-block: uint,
        start-block: uint,
        end-block: uint,
        cliff-block: (optional uint),
        last-withdrawal-block: uint,
        total-withdrawn: uint,
        status: (string-ascii 20),
        vesting-type: (string-ascii 20),
        can-modify: bool,
        metadata: (string-utf8 256),
        created-at: uint
    }
)

;; ============================================
;; Data Maps - Stream Indexes
;; ============================================
(define-map employer-streams principal (list 200 uint))
(define-map employee-streams principal (list 200 uint))

;; ============================================
;; Data Maps - Stream Modifications History
;; ============================================
(define-map stream-modifications
    { stream-id: uint }
    (list 10 { 
        block: uint, 
        field: (string-ascii 20), 
        old-value: uint, 
        new-value: uint,
        by: principal
    })
)

;; ============================================
;; Data Maps - Pause Tracking
;; ============================================
(define-map stream-pause-data
    { stream-id: uint }
    {
        total-paused-blocks: uint,
        last-pause-block: (optional uint)
    }
)

;; ============================================
;; Read-Only Functions - Stream Queries
;; ============================================

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

(define-read-only (get-employer-streams (employer principal))
    (default-to (list) (map-get? employer-streams employer))
)

(define-read-only (get-employee-streams (employee principal))
    (default-to (list) (map-get? employee-streams employee))
)

(define-read-only (get-stream-modifications (stream-id uint))
    (default-to (list) (map-get? stream-modifications { stream-id: stream-id }))
)

(define-read-only (get-next-stream-id)
    (var-get next-stream-id)
)

(define-read-only (get-total-streams-created)
    (var-get total-streams-created)
)

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

(define-read-only (get-earned-amount (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (current-block block-height)
            (rate (get rate-per-block stream))
            (last-block (get last-withdrawal-block stream))
            (total-amount (get total-amount stream))
            (total-withdrawn (get total-withdrawn stream))
            (vesting-type (get vesting-type stream))
            (start-block (get start-block stream))
            (end-block (get end-block stream))
            (cliff-block (get cliff-block stream))
        )
        ;; Calculate time-based earned
        (let
            (
                (blocks-elapsed (if (> current-block last-block) (- current-block last-block) u0))
                (time-earned (* blocks-elapsed rate))
                (total-time-earned (+ total-withdrawn time-earned))
            )
            ;; Apply vesting limits
            (if (is-eq vesting-type VESTING_LINEAR)
                (let
                    (
                        (vested (unwrap-panic (contract-call? .stream-math calculate-linear-vested 
                            total-amount start-block end-block current-block)))
                    )
                    (ok (if (< total-time-earned vested) time-earned (- vested total-withdrawn)))
                )
                (if (is-eq vesting-type VESTING_CLIFF_LINEAR)
                    (let
                        (
                            (cliff (unwrap! cliff-block ERR_INVALID_VESTING_TYPE))
                            (vested (unwrap-panic (contract-call? .stream-math calculate-cliff-vested 
                                total-amount start-block cliff end-block current-block)))
                        )
                        (ok (if (< total-time-earned vested) time-earned (- vested total-withdrawn)))
                    )
                    ;; No vesting, just time-based
                    (ok time-earned)
                )
            )
        )
    )
)

(define-read-only (get-withdrawable-amount (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (earned (unwrap-panic (get-earned-amount stream-id)))
        )
        (ok earned)
    )
)

(define-read-only (get-remaining-amount (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
        )
        (ok (- (get total-amount stream) (get total-withdrawn stream)))
    )
)

(define-read-only (get-stream-status (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
        )
        (ok (get status stream))
    )
)

;; ============================================
;; Private Functions - Helpers
;; ============================================

(define-private (add-stream-to-employer (employer principal) (stream-id uint))
    (let
        (
            (current-streams (default-to (list) (map-get? employer-streams employer)))
        )
        (map-set employer-streams employer 
            (unwrap-panic (as-max-len? (append current-streams stream-id) u200)))
    )
)

(define-private (add-stream-to-employee (employee principal) (stream-id uint))
    (let
        (
            (current-streams (default-to (list) (map-get? employee-streams employee)))
        )
        (map-set employee-streams employee 
            (unwrap-panic (as-max-len? (append current-streams stream-id) u200)))
    )
)

(define-private (add-modification-record 
    (stream-id uint) 
    (field (string-ascii 20)) 
    (old-value uint) 
    (new-value uint))
    (let
        (
            (current-mods (default-to (list) (map-get? stream-modifications { stream-id: stream-id })))
            (new-mod { 
                block: block-height, 
                field: field, 
                old-value: old-value, 
                new-value: new-value,
                by: tx-sender
            })
        )
        (map-set stream-modifications { stream-id: stream-id }
            (unwrap-panic (as-max-len? (append current-mods new-mod) u10)))
    )
)

;; ============================================
;; Public Functions - Stream Creation
;; ============================================

(define-public (create-stream
    (employee principal)
    (total-amount uint)
    (duration-blocks uint)
    (vesting-type (string-ascii 20))
    (cliff-blocks (optional uint))
    (can-modify bool)
    (metadata (string-utf8 256)))
    (let
        (
            (employer tx-sender)
            (stream-id (var-get next-stream-id))
            (start-block block-height)
            (end-block (+ start-block duration-blocks))
            (cliff-block (match cliff-blocks
                blocks (some (+ start-block blocks))
                none))
            (rate-per-block (unwrap-panic (contract-call? .stream-math calculate-rate-per-block 
                total-amount duration-blocks)))
        )
        ;; Validations
        (asserts! (contract-call? .access-control is-employer employer) ERR_UNAUTHORIZED)
        (asserts! (> total-amount u0) ERR_INVALID_AMOUNT)
        (asserts! (> duration-blocks u0) ERR_INVALID_DURATION)
        (asserts! (not (is-eq employee employer)) ERR_INVALID_EMPLOYEE)
        
        ;; Check employer has sufficient balance in treasury
        (asserts! (>= (contract-call? .treasury get-employer-stx-balance employer) 
            total-amount) ERR_INSUFFICIENT_FUNDS)
        
        ;; Create stream
        (map-set streams { stream-id: stream-id }
            {
                employer: employer,
                employee: employee,
                token-contract: none,
                total-amount: total-amount,
                rate-per-block: rate-per-block,
                start-block: start-block,
                end-block: end-block,
                cliff-block: cliff-block,
                last-withdrawal-block: start-block,
                total-withdrawn: u0,
                status: STATUS_ACTIVE,
                vesting-type: vesting-type,
                can-modify: can-modify,
                metadata: metadata,
                created-at: block-height
            }
        )
        
        ;; Update indexes
        (add-stream-to-employer employer stream-id)
        (add-stream-to-employee employee stream-id)
        
        ;; Update counters
        (var-set next-stream-id (+ stream-id u1))
        (var-set total-streams-created (+ (var-get total-streams-created) u1))
        
        ;; Emit event
        (print {
            event: "stream-created",
            stream-id: stream-id,
            employer: employer,
            employee: employee,
            total-amount: total-amount,
            duration-blocks: duration-blocks,
            rate-per-block: rate-per-block,
            vesting-type: vesting-type,
            block: block-height
        })
        
        (ok stream-id)
    )
)

;; ============================================
;; Public Functions - Withdrawals
;; ============================================

(define-public (withdraw (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (employee tx-sender)
            (employer (get employer stream))
            (earned (unwrap-panic (get-earned-amount stream-id)))
        )
        ;; Validations
        (asserts! (is-eq employee (get employee stream)) ERR_UNAUTHORIZED)
        (asserts! (is-eq (get status stream) STATUS_ACTIVE) ERR_STREAM_INACTIVE)
        (asserts! (> earned u0) ERR_NOTHING_TO_WITHDRAW)
        
        ;; Check if stream is paused
        (asserts! (not (contract-call? .emergency-controls is-stream-paused stream-id)) 
            ERR_STREAM_PAUSED)
        (asserts! (contract-call? .emergency-controls is-operational) 
            ERR_SYSTEM_PAUSED)
        
        ;; Withdraw from treasury
        (try! (contract-call? .treasury withdraw-stx employer earned employee))
        
        ;; Update stream
        (let
            (
                (new-total-withdrawn (+ (get total-withdrawn stream) earned))
                (new-status (if (is-eq new-total-withdrawn (get total-amount stream))
                    STATUS_COMPLETED
                    STATUS_ACTIVE))
            )
            (map-set streams { stream-id: stream-id }
                (merge stream {
                    last-withdrawal-block: block-height,
                    total-withdrawn: new-total-withdrawn,
                    status: new-status
                })
            )
            
            ;; Update global counter
            (var-set total-amount-streamed (+ (var-get total-amount-streamed) earned))
            
            ;; Emit event
            (print {
                event: "withdrawal",
                stream-id: stream-id,
                employee: employee,
                amount: earned,
                total-withdrawn: new-total-withdrawn,
                status: new-status,
                block: block-height
            })
            
            (ok earned)
        )
    )
)

;; ============================================
;; Public Functions - Stream Modification
;; ============================================

(define-public (extend-stream (stream-id uint) (additional-blocks uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (employer tx-sender)
        )
        ;; Validations
        (asserts! (is-eq employer (get employer stream)) ERR_UNAUTHORIZED)
        (asserts! (get can-modify stream) ERR_MODIFICATION_NOT_ALLOWED)
        (asserts! (is-eq (get status stream) STATUS_ACTIVE) ERR_STREAM_INACTIVE)
        (asserts! (> additional-blocks u0) ERR_INVALID_DURATION)
        
        ;; Update stream
        (let
            (
                (old-end (get end-block stream))
                (new-end (+ old-end additional-blocks))
                (new-rate (unwrap-panic (contract-call? .stream-math calculate-rate-per-block
                    (- (get total-amount stream) (get total-withdrawn stream))
                    (- new-end block-height))))
            )
            (map-set streams { stream-id: stream-id }
                (merge stream {
                    end-block: new-end,
                    rate-per-block: new-rate
                })
            )
            
            ;; Record modification
            (add-modification-record stream-id "end-block" old-end new-end)
            
            ;; Emit event
            (print {
                event: "stream-extended",
                stream-id: stream-id,
                old-end-block: old-end,
                new-end-block: new-end,
                additional-blocks: additional-blocks,
                block: block-height
            })
            
            (ok true)
        )
    )
)

(define-public (increase-stream-amount (stream-id uint) (additional-amount uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (employer tx-sender)
        )
        ;; Validations
        (asserts! (is-eq employer (get employer stream)) ERR_UNAUTHORIZED)
        (asserts! (get can-modify stream) ERR_MODIFICATION_NOT_ALLOWED)
        (asserts! (is-eq (get status stream) STATUS_ACTIVE) ERR_STREAM_INACTIVE)
        (asserts! (> additional-amount u0) ERR_INVALID_AMOUNT)
        
        ;; Check employer has sufficient balance
        (asserts! (>= (contract-call? .treasury get-employer-stx-balance employer) 
            additional-amount) ERR_INSUFFICIENT_FUNDS)
        
        ;; Update stream
        (let
            (
                (old-amount (get total-amount stream))
                (new-amount (+ old-amount additional-amount))
                (remaining-blocks (- (get end-block stream) block-height))
                (new-rate (unwrap-panic (contract-call? .stream-math calculate-rate-per-block
                    (- new-amount (get total-withdrawn stream))
                    remaining-blocks)))
            )
            (map-set streams { stream-id: stream-id }
                (merge stream {
                    total-amount: new-amount,
                    rate-per-block: new-rate
                })
            )
            
            ;; Record modification
            (add-modification-record stream-id "total-amount" old-amount new-amount)
            
            ;; Emit event
            (print {
                event: "stream-amount-increased",
                stream-id: stream-id,
                old-amount: old-amount,
                new-amount: new-amount,
                additional-amount: additional-amount,
                block: block-height
            })
            
            (ok true)
        )
    )
)

;; ============================================
;; Public Functions - Stream Control
;; ============================================

(define-public (cancel-stream (stream-id uint))
    (let
        (
            (stream (unwrap! (get-stream stream-id) ERR_STREAM_NOT_FOUND))
            (employer tx-sender)
            (employee (get employee stream))
        )
        ;; Validations
        (asserts! (is-eq employer (get employer stream)) ERR_UNAUTHORIZED)
        (asserts! (is-eq (get status stream) STATUS_ACTIVE) ERR_STREAM_INACTIVE)
        
        ;; Calculate refund (unvested/unearned amount)
        (let
            (
                (total-amount (get total-amount stream))
                (total-withdrawn (get total-withdrawn stream))
                (earned (unwrap-panic (get-earned-amount stream-id)))
                (refund-amount (- total-amount total-withdrawn earned))
            )
            ;; If there's earned but not withdrawn, transfer to employee first
            (if (> earned u0)
                (try! (contract-call? .treasury withdraw-stx employer earned employee))
                true
            )
            
            ;; Update stream status
            (map-set streams { stream-id: stream-id }
                (merge stream {
                    status: STATUS_CANCELLED,
                    total-withdrawn: (+ total-withdrawn earned)
                })
            )
            
            ;; Emit event
            (print {
                event: "stream-cancelled",
                stream-id: stream-id,
                refund-amount: refund-amount,
                final-payment: earned,
                block: block-height
            })
            
            (ok refund-amount)
        )
    )
)

;; ============================================
;; Public Functions - Batch Operations
;; ============================================

(define-public (batch-create-streams
    (employees (list 50 principal))
    (amounts (list 50 uint))
    (durations (list 50 uint))
    (vesting-type (string-ascii 20))
    (can-modify bool))
    (let
        (
            (employer tx-sender)
        )
        (asserts! (contract-call? .access-control is-employer employer) ERR_UNAUTHORIZED)
        
        (ok (map batch-create-stream-internal 
            employees 
            amounts 
            durations))
    )
)

(define-private (batch-create-stream-internal 
    (employee principal) 
    (amount uint) 
    (duration uint))
    (unwrap-panic (create-stream employee amount duration VESTING_LINEAR none true u"Batch created"))
)

;; ============================================
;; Public Functions - Bonus Payments
;; ============================================

(define-public (add-bonus (employee principal) (amount uint) (metadata (string-utf8 256)))
    (let
        (
            (employer tx-sender)
        )
        ;; Validations
        (asserts! (contract-call? .access-control is-employer employer) ERR_UNAUTHORIZED)
        (asserts! (> amount u0) ERR_INVALID_AMOUNT)
        (asserts! (>= (contract-call? .treasury get-employer-stx-balance employer) 
            amount) ERR_INSUFFICIENT_FUNDS)
        
        ;; Transfer bonus immediately
        (try! (contract-call? .treasury withdraw-stx employer amount employee))
        
        ;; Emit event
        (print {
            event: "bonus-paid",
            employer: employer,
            employee: employee,
            amount: amount,
            metadata: metadata,
            block: block-height
        })
        
        (ok true)
    )
)

Functions (21)

FunctionAccessArgs
get-streamread-onlystream-id: uint
get-employer-streamsread-onlyemployer: principal
get-employee-streamsread-onlyemployee: principal
get-stream-modificationsread-onlystream-id: uint
get-next-stream-idread-only
get-total-streams-createdread-only
get-earned-amountread-onlystream-id: uint
get-withdrawable-amountread-onlystream-id: uint
get-remaining-amountread-onlystream-id: uint
get-stream-statusread-onlystream-id: uint
add-stream-to-employerprivateemployer: principal, stream-id: uint
add-stream-to-employeeprivateemployee: principal, stream-id: uint
add-modification-recordprivatestream-id: uint, field: (string-ascii 20
create-streampublicemployee: principal, total-amount: uint, duration-blocks: uint, vesting-type: (string-ascii 20
withdrawpublicstream-id: uint
extend-streampublicstream-id: uint, additional-blocks: uint
increase-stream-amountpublicstream-id: uint, additional-amount: uint
cancel-streampublicstream-id: uint
batch-create-streamspublicemployees: (list 50 principal
batch-create-stream-internalprivateemployee: principal, amount: uint, duration: uint
add-bonuspublicemployee: principal, amount: uint, metadata: (string-utf8 256