Source Code

;; title: StackPay Core Contract Architecture
;; summary: Main contract that handles payment invoice creation, tracking, and settlement
;; description: This contract architecture defines the core functionality of the StackPay payment processor with secure input handling.

;; traits
(define-trait invoice-trait (
  (get-invoice
    ((string-ascii 64))
    (
      response
      (optional {
      merchant: principal,
      recipient: principal,
      amount: uint,
      currency: (string-ascii 10),
      status: uint,
      created-at: uint,
      expires-at: uint,
      paid-at: (optional uint),
      description: (string-utf8 256),
      metadata: (string-utf8 512),
      payment-address: (optional principal),
      webhook-url: (optional (string-ascii 256)),
    })
      uint
    )
  )
))

;; constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_UNAUTHORIZED (err u100))
(define-constant ERR_INVOICE_NOT_FOUND (err u101))
(define-constant ERR_INVOICE_EXPIRED (err u102))
(define-constant ERR_INVOICE_ALREADY_PAID (err u103))
(define-constant ERR_INSUFFICIENT_PAYMENT (err u104))
(define-constant ERR_INVALID_AMOUNT (err u105))
(define-constant ERR_REFUND_FAILED (err u106))
(define-constant ERR_INVALID_INPUT (err u107))
(define-constant ERR_INVALID_WEBHOOK (err u108))
(define-constant ERR_INVALID_PRINCIPAL (err u109))
(define-constant ERR_INACTIVE_MERCHANT (err u110))

(define-constant STATUS_PENDING u0)
(define-constant STATUS_PAID u1)
(define-constant STATUS_EXPIRED u2)
(define-constant STATUS_REFUNDED u3)

(define-constant PLATFORM_FEE_BPS u250) ;; 2.5%
(define-constant MAX_INVOICE_EXPIRY u52560) ;; ~1 year in blocks
(define-constant MAX_WEBHOOK_LENGTH u256)
(define-constant MAX_DESCRIPTION_LENGTH u256)
(define-constant MAX_METADATA_LENGTH u512)

;; data maps
(define-map invoices
  { invoice-id: (string-ascii 64) }
  {
    merchant: principal,
    recipient: principal,
    amount: uint,
    currency: (string-ascii 10),
    status: uint,
    created-at: uint,
    expires-at: uint,
    paid-at: (optional uint),
    description: (string-utf8 256),
    metadata: (string-utf8 512),
    payment-address: (optional principal),
    webhook-url: (optional (string-ascii 256)),
  }
)

(define-map merchants
  { merchant: principal }
  {
    is-active: bool,
    webhook-url: (optional (string-ascii 256)),
    api-key-hash: (buff 32),
    fee-recipient: (optional principal),
    created-at: uint,
  }
)

(define-map payment-receipts
  { receipt-id: (string-ascii 64) }
  {
    invoice-id: (string-ascii 64),
    payer: principal,
    amount-paid: uint,
    tx-id: (buff 32),
    block-height: uint,
    timestamp: uint,
  }
)

;; data vars
(define-data-var invoice-counter uint u0)
(define-data-var receipt-counter uint u0)
(define-data-var platform-fee-recipient principal CONTRACT_OWNER)
(define-data-var is-paused bool false)

;; private functions
(define-private (generate-invoice-id)
  (let ((counter (var-get invoice-counter)))
    (var-set invoice-counter (+ counter u1))
    (concat "INV_" (int-to-ascii (+ stacks-block-height counter)))
  )
)

(define-private (generate-receipt-id)
  (let ((counter (var-get receipt-counter)))
    (var-set receipt-counter (+ counter u1))
    (concat "RCP_" (int-to-ascii (+ stacks-block-height counter)))
  )
)

(define-private (calculate-platform-fee (amount uint))
  (let ((fee (/ (* amount PLATFORM_FEE_BPS) u10000)))
    (if (> fee amount)
      u0
      fee
    )
  )
)

(define-private (is-invoice-expired (expires-at uint))
  (> stacks-block-height expires-at)
)

(define-private (validate-currency (c (string-ascii 10)))
  (or (is-eq c "STX") (is-eq c "SBTC"))
)

(define-private (validate-principal (p principal))
  (and
    (not (is-eq p 'SP000000000000000000002Q6VF78)) ;; Burn address
    (not (is-eq p 'ST000000000000000000002AMW42H)) ;; Reserved address
    true
  )
)

(define-private (validate-merchant (m principal))
  (match (map-get? merchants { merchant: m })
    merchant-data (get is-active merchant-data)
    false
  )
)


(define-private (validate-webhook (webhook (optional (string-ascii 256))))
  (match webhook
    url (and
      (<= (len url) MAX_WEBHOOK_LENGTH)
      (> (len url) u8) ;; Ensure URL is longer than just "https://"
      (is-eq (slice? url u0 u8) (some "https://")) ;; Check if URL starts with "https://"
      true
    )
    true
  )
)

;; public functions
(define-public (register-merchant
    (webhook-url (optional (string-ascii 256)))
    (api-key-hash (buff 32))
  )
  (let ((merchant tx-sender))
    (asserts! (validate-principal merchant) ERR_INVALID_PRINCIPAL)
    (asserts! (validate-webhook webhook-url) ERR_INVALID_WEBHOOK)
    (asserts! (> (len api-key-hash) u0) ERR_INVALID_INPUT)
    (map-set merchants { merchant: merchant } {
      is-active: true,
      webhook-url: webhook-url,
      api-key-hash: api-key-hash,
      fee-recipient: none,
      created-at: stacks-block-height,
    })
    (print {
      event: "merchant-registered",
      merchant: merchant,
      block-height: stacks-block-height,
    })
    (ok merchant)
  )
)

(define-public (create-invoice
    (recipient principal)
    (amount uint)
    (currency (string-ascii 10))
    (expires-in-blocks uint)
    (description (string-utf8 256))
    (metadata (string-utf8 512))
    (webhook-url (optional (string-ascii 256)))
  )
  (let (
      (merchant tx-sender)
      (invoice-id (generate-invoice-id))
      (current-height stacks-block-height)
      (expires-at (+ current-height expires-in-blocks))
    )
    (asserts! (validate-principal merchant) ERR_INVALID_PRINCIPAL)
    (asserts! (validate-merchant merchant) ERR_INACTIVE_MERCHANT)
    (asserts! (validate-principal recipient) ERR_INVALID_PRINCIPAL)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (validate-currency currency) ERR_INVALID_INPUT)
    (asserts! (<= expires-in-blocks MAX_INVOICE_EXPIRY) ERR_INVALID_INPUT)
    (asserts! (<= (len description) MAX_DESCRIPTION_LENGTH) ERR_INVALID_INPUT)
    (asserts! (<= (len metadata) MAX_METADATA_LENGTH) ERR_INVALID_INPUT)
    (asserts! (> (len currency) u0) ERR_INVALID_INPUT)
    (asserts! (validate-webhook webhook-url) ERR_INVALID_WEBHOOK)
    (asserts! (not (var-get is-paused)) ERR_UNAUTHORIZED)
    (map-set invoices { invoice-id: invoice-id } {
      merchant: merchant,
      recipient: recipient,
      amount: amount,
      currency: currency,
      status: STATUS_PENDING,
      created-at: current-height,
      expires-at: expires-at,
      paid-at: none,
      description: description,
      metadata: metadata,
      payment-address: none,
      webhook-url: webhook-url,
    })
    (print {
      event: "invoice-created",
      invoice-id: invoice-id,
      merchant: merchant,
      recipient: recipient,
      amount: amount,
      currency: currency,
      expires-at: expires-at,
      block-height: current-height,
    })
    (ok invoice-id)
  )
)

(define-public (process-payment
    (invoice-id (string-ascii 64))
    (payer principal)
    (amount-paid uint)
    (tx-id (buff 32))
  )
  (let (
      (invoice-data (unwrap! (map-get? invoices { invoice-id: invoice-id })
        ERR_INVOICE_NOT_FOUND
      ))
      (receipt-id (generate-receipt-id))
      (current-height stacks-block-height)
    )
    (asserts! (validate-principal payer) ERR_INVALID_PRINCIPAL)
    (asserts! (> (len invoice-id) u0) ERR_INVALID_INPUT)
    (asserts! (> (len tx-id) u0) ERR_INVALID_INPUT)
    (asserts! (> amount-paid u0) ERR_INVALID_AMOUNT)
    (asserts! (is-eq (get status invoice-data) STATUS_PENDING)
      ERR_INVOICE_ALREADY_PAID
    )
    (asserts! (not (is-invoice-expired (get expires-at invoice-data)))
      ERR_INVOICE_EXPIRED
    )
    (asserts! (>= amount-paid (get amount invoice-data)) ERR_INSUFFICIENT_PAYMENT)
    ;; Update invoice status
    (map-set invoices { invoice-id: invoice-id }
      (merge invoice-data {
        status: STATUS_PAID,
        paid-at: (some current-height),
      })
    )
    ;; Create payment receipt
    (map-set payment-receipts { receipt-id: receipt-id } {
      invoice-id: invoice-id,
      payer: payer,
      amount-paid: amount-paid,
      tx-id: tx-id,
      block-height: current-height,
      timestamp: current-height,
    })
    ;; Calculate and handle fees
    (let (
        (platform-fee (calculate-platform-fee amount-paid))
        (merchant-amount (- amount-paid platform-fee))
      )
      (asserts! (>= amount-paid platform-fee) ERR_INVALID_AMOUNT)
      ;; Transfer platform fee
      (if (> platform-fee u0)
        (try! (stx-transfer? platform-fee tx-sender (var-get platform-fee-recipient)))
        true
      )
      ;; Transfer remaining amount to recipient
      (try! (stx-transfer? merchant-amount tx-sender (get recipient invoice-data)))
      (print {
        event: "payment-processed",
        invoice-id: invoice-id,
        receipt-id: receipt-id,
        payer: payer,
        amount-paid: amount-paid,
        platform-fee: platform-fee,
        merchant-amount: merchant-amount,
        block-height: current-height,
      })
      (ok receipt-id)
    )
  )
)

(define-public (expire-invoice (invoice-id (string-ascii 64)))
  (let ((invoice-data (unwrap! (map-get? invoices { invoice-id: invoice-id }) ERR_INVOICE_NOT_FOUND)))
    (asserts! (> (len invoice-id) u0) ERR_INVALID_INPUT)
    (asserts! (is-eq (get status invoice-data) STATUS_PENDING)
      ERR_INVOICE_ALREADY_PAID
    )
    ;; (asserts! (is-invoice-expired (get expires-at invoice-data))
    ;;   ERR_INVOICE_NOT_FOUND
    ;; )
    ;; Update status to expired
    (map-set invoices { invoice-id: invoice-id }
      (merge invoice-data { status: STATUS_EXPIRED })
    )
    (print {
      event: "invoice-expired",
      invoice-id: invoice-id,
      block-height: stacks-block-height,
    })
    (ok true)
  )
)

;; admin functions
(define-public (set-platform-fee-recipient (new-recipient principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (asserts! (validate-principal new-recipient) ERR_INVALID_PRINCIPAL)
    (var-set platform-fee-recipient new-recipient)
    (ok true)
  )
)

(define-public (set-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (var-set is-paused paused)
    (ok true)
  )
)

;; read only functions
(define-read-only (get-invoice (invoice-id (string-ascii 64)))
  (begin
    (map-get? invoices { invoice-id: invoice-id })
  )
)

(define-read-only (get-receipt (receipt-id (string-ascii 64)))
  (begin
    (map-get? payment-receipts { receipt-id: receipt-id })
  )
)

(define-read-only (get-merchant (merchant principal))
  (begin
    (map-get? merchants { merchant: merchant })
  )
)

(define-read-only (is-invoice-payable (invoice-id (string-ascii 64)))
  (begin
    (match (map-get? invoices { invoice-id: invoice-id })
      invoice-data (and
        (is-eq (get status invoice-data) STATUS_PENDING)
        (not (is-invoice-expired (get expires-at invoice-data)))
      )
      false
    )
  )
)

Functions (18)

FunctionAccessArgs
generate-invoice-idprivate
generate-receipt-idprivate
calculate-platform-feeprivateamount: uint
is-invoice-expiredprivateexpires-at: uint
validate-currencyprivatec: (string-ascii 10
validate-principalprivatep: principal
validate-merchantprivatem: principal
validate-webhookprivatewebhook: (optional (string-ascii 256
register-merchantpublicwebhook-url: (optional (string-ascii 256
create-invoicepublicrecipient: principal, amount: uint, currency: (string-ascii 10
process-paymentpublicinvoice-id: (string-ascii 64
expire-invoicepublicinvoice-id: (string-ascii 64
set-platform-fee-recipientpublicnew-recipient: principal
set-pausedpublicpaused: bool
get-invoiceread-onlyinvoice-id: (string-ascii 64
get-receiptread-onlyreceipt-id: (string-ascii 64
get-merchantread-onlymerchant: principal
is-invoice-payableread-onlyinvoice-id: (string-ascii 64