Source Code

;; title: stackflow
;; author: brice.btc
;; version: 0.6.0
;; summary: Stackflow is a payment channel network built on Stacks, enabling
;;   off-chain, non-custodial, and high-speed payments between users. Designed
;;   to be simple, secure, and efficient, it supports transactions in STX and
;;   SIP-010 fungible tokens.

;; MIT License

;; Copyright (c) 2024-2026 obycode, LLC

;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:

;; The above copyright notice and this permission notice shall be included in all
;; copies or substantial portions of the Software.

;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.

(use-trait sip-010 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
(impl-trait 'SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-token-0-6-0.stackflow-token)

(define-constant CONTRACT_DEPLOYER tx-sender)
(define-constant MAX_HEIGHT u340282366920938463463374607431768211455)
(define-constant WAITING_PERIOD u144) ;; 24 hours in blocks

;; Constants for SIP-018 structured data
(define-constant structured-data-prefix 0x534950303138)
(define-constant message-domain-hash (sha256 (unwrap-panic (to-consensus-buff? {
  name: (unwrap-panic (to-ascii? current-contract)),
  version: "0.6.0",
  chain-id: chain-id,
}))))
(define-constant structured-data-header (concat structured-data-prefix message-domain-hash))

;; Actions
(define-constant ACTION_CLOSE u0)
(define-constant ACTION_TRANSFER u1)
(define-constant ACTION_DEPOSIT u2)
(define-constant ACTION_WITHDRAWAL u3)

;; Error codes
(define-constant ERR_DEPOSIT_FAILED (err u100))
(define-constant ERR_NO_SUCH_PIPE (err u101))
(define-constant ERR_INVALID_PRINCIPAL (err u102))
(define-constant ERR_INVALID_SENDER_SIGNATURE (err u103))
(define-constant ERR_INVALID_OTHER_SIGNATURE (err u104))
(define-constant ERR_CONSENSUS_BUFF (err u105))
(define-constant ERR_UNAUTHORIZED (err u106))
(define-constant ERR_INVALID_TOTAL_BALANCE (err u108))
(define-constant ERR_WITHDRAWAL_FAILED (err u109))
(define-constant ERR_PIPE_EXPIRED (err u110))
(define-constant ERR_NONCE_TOO_LOW (err u111))
(define-constant ERR_CLOSE_IN_PROGRESS (err u112))
(define-constant ERR_NO_CLOSE_IN_PROGRESS (err u113))
(define-constant ERR_SELF_DISPUTE (err u114))
(define-constant ERR_ALREADY_FUNDED (err u115))
(define-constant ERR_INVALID_WITHDRAWAL (err u116))
(define-constant ERR_UNAPPROVED_TOKEN (err u117))
(define-constant ERR_NOT_EXPIRED (err u118))
(define-constant ERR_NOT_INITIALIZED (err u119))
(define-constant ERR_ALREADY_INITIALIZED (err u120))
(define-constant ERR_NOT_VALID_YET (err u121))
(define-constant ERR_ALREADY_PENDING (err u122))
(define-constant ERR_PENDING (err u123))
(define-constant ERR_INVALID_BALANCES (err u124))
(define-constant ERR_INVALID_SIGNATURE (err u125))
(define-constant ERR_ALLOWANCE_VIOLATION (err u126))
(define-constant ERR_SELF_PIPE (err u127))

;; Number of burn blocks to wait before considering an on-chain action finalized.
(define-constant CONFIRMATION_DEPTH u6)

;;; Has this contract been initialized?
(define-data-var initialized bool false)

;;; The token supported by this instance of the Stackflow contract.
;;; If `none`, only STX is supported.
(define-data-var supported-token (optional principal) none)

;;; Map tracking the initial balances in pipes between two principals for a
;;; given token.
(define-map pipes
  {
    token: (optional principal),
    principal-1: principal,
    principal-2: principal,
  }
  {
    balance-1: uint,
    balance-2: uint,
    pending-1: (optional {
      amount: uint,
      burn-height: uint,
    }),
    pending-2: (optional {
      amount: uint,
      burn-height: uint,
    }),
    expires-at: uint,
    nonce: uint,
    closer: (optional principal),
  }
)

;; Mapping of principals to agents registered to act on their behalf
(define-map agents
  principal
  principal
)

;; Public Functions
;;

;;; Initialize the contract with the supported token.
;;; Returns:
;;; - `(ok true)` on success
;;; - `ERR_ALREADY_INITIALIZED` if the contract has already been initialized
;;; - `ERR_UNAUTHORIZED` if the sender is not the contract deployer
(define-public (init (token (optional <sip-010>)))
  (begin
    (asserts! (not (var-get initialized)) ERR_ALREADY_INITIALIZED)
    (asserts! (is-eq tx-sender CONTRACT_DEPLOYER) ERR_UNAUTHORIZED)
    (var-set supported-token (contract-of-optional token))
    (ok (var-set initialized true))
  )
)

;;; Register a signing key for the calling contract principal. This is intended
;;; for contract participants (e.g. Reservoir contracts) that cannot produce
;;; signatures directly. Standard principals are not allowed to register agents.
;;; Returns `(ok true)`
(define-public (register-agent (agent principal))
  (begin
    (asserts! (is-contract-principal contract-caller) ERR_UNAUTHORIZED)
    (ok (map-set agents contract-caller agent))
  )
)

;;; Deregister the signing key for the calling contract principal.
;;; Returns:
;;; - `(ok true)` if an agent had been registered
;;; - `(ok false)` if there was no agent registered
(define-public (deregister-agent)
  (begin
    (asserts! (is-contract-principal contract-caller) ERR_UNAUTHORIZED)
    (ok (map-delete agents contract-caller))
  )
)

;;; Deposit `amount` funds into an unfunded pipe between `tx-sender` and
;;; `with` for FT `token` (`none` indicates STX). Create the pipe if one
;;; does not already exist.
;;; Returns:
;;; - The pipe key on success
;;;   ```
;;;   { token: (optional principal), principal-1: principal, principal-2: principal }
;;;   ```
;;; - `ERR_NOT_INITIALIZED` if the contract has not been initialized
;;; - `ERR_UNAPPROVED_TOKEN` if the token is not the correct token
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_CLOSE_IN_PROGRESS` if a forced closure is in progress
;;; - `ERR_ALREADY_FUNDED` if the pipe has already been funded
(define-public (fund-pipe
    (token (optional <sip-010>))
    (amount uint)
    (with principal)
    (nonce uint)
  )
  (begin
    (try! (check-token token))
    (asserts! (not (is-eq tx-sender with)) ERR_SELF_PIPE)
    (let (
        (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
        (existing-pipe (map-get? pipes pipe-key))
        (pipe (match existing-pipe
          ch ch
          {
            balance-1: u0,
            balance-2: u0,
            pending-1: none,
            pending-2: none,
            expires-at: MAX_HEIGHT,
            nonce: nonce,
            closer: none,
          }
        ))
        (settled-pipe (settle-pending pipe))
        (updated-pipe (try! (increase-sender-balance pipe-key settled-pipe token amount)))
        (closer (get closer settled-pipe))
      )
      ;; If there was an existing pipe, the new nonce must be equal or greater
      (asserts! (>= nonce (get nonce pipe)) ERR_NONCE_TOO_LOW)

      ;; A forced closure must not be in progress
      (asserts! (is-none closer) ERR_CLOSE_IN_PROGRESS)

      ;; Only fund a pipe with a 0 balance for the sender can be funded. After
      ;; the pipe is initially funded, additional funds must use the `deposit`
      ;; function, which requires signatures from both parties.
      (asserts! (not (is-funded tx-sender pipe-key updated-pipe))
        ERR_ALREADY_FUNDED
      )
      (map-set pipes pipe-key updated-pipe)

      ;; Emit an event
      (print {
        event: "fund-pipe",
        pipe-key: pipe-key,
        pipe: updated-pipe,
        sender: tx-sender,
        amount: amount,
      })
      (ok pipe-key)
    )
  )
)

;;; Cooperatively close the pipe, with authorization from both parties.
;;; Returns:
;;; - `(ok true)` on success
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the sum of the balances provided
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
(define-public (close-pipe
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
      (signed-balances (map-balances tx-sender pipe-key my-balance their-balance))
      (balance-1 (get balance-1 signed-balances))
      (balance-2 (get balance-2 signed-balances))
      (updated-pipe {
        balance-1: balance-1,
        balance-2: balance-2,
        expires-at: MAX_HEIGHT,
        nonce: nonce,
        closer: none,
      })
    )
    (try! (validate-transition pipe-key balance-1 balance-2 nonce ACTION_CLOSE
      tx-sender u0 none
    ))

    ;; Verify the signatures of the two parties.
    (try! (verify-signatures my-signature tx-sender their-signature with pipe-key
      balance-1 balance-2 nonce ACTION_CLOSE tx-sender none none
    ))

    ;; Reset the pipe in the map.
    (reset-pipe pipe-key nonce)

    ;; Emit an event
    (print {
      event: "close-pipe",
      pipe-key: pipe-key,
      pipe: updated-pipe,
      sender: tx-sender,
    })

    ;; Pay out the balances.
    (payout token tx-sender with my-balance their-balance)
  )
)

;;; Close the pipe and return the original balances to both participants.
;;; This initiates a waiting period, giving the other party the opportunity to
;;; dispute the closing of the pipe, by calling `dispute-closure` and
;;; providing signatures proving a transfer.
;;; Returns:
;;; - `(ok expires-at)` on success, where `expires-at` is the block height at
;;;   which the pipe can be finalized if it has not been disputed.
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_CLOSE_IN_PROGRESS` if a forced closure is already in progress
(define-public (force-cancel
    (token (optional <sip-010>))
    (with principal)
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (closer (get closer pipe))
      (expires-at (+ burn-block-height WAITING_PERIOD))
      (settled-pipe (settle-pending pipe))
    )
    ;; A forced closure must not be in progress
    (asserts! (is-none closer) ERR_CLOSE_IN_PROGRESS)

    ;; Cannot cancel a pipe while there is a pending deposit
    (asserts!
      (and
        (is-none (get pending-1 settled-pipe))
        (is-none (get pending-2 settled-pipe))
      )
      ERR_PENDING
    )

    ;; Set the waiting period for this pipe.
    (map-set pipes pipe-key
      (merge settled-pipe {
        expires-at: expires-at,
        closer: (some tx-sender),
      })
    )

    ;; Emit an event
    (print {
      event: "force-cancel",
      pipe-key: pipe-key,
      pipe: settled-pipe,
      sender: tx-sender,
    })

    (ok expires-at)
  )
)

;;; Close the pipe using signatures from the most recent transfer.
;;; This initiates a waiting period, giving the other party the opportunity to
;;; dispute the closing of the pipe, by calling `dispute-closure` and
;;; providing signatures with a later nonce.
;;; Returns:
;;; - `(ok expires-at)` on success, where `expires-at` is the block height at
;;;   which the pipe can be finalized if it has not been disputed.
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_CLOSE_IN_PROGRESS` if a forced closure is already in progress
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the sum of the balances provided
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
(define-public (force-close
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (pipe-nonce (get nonce pipe))
      (closer (get closer pipe))
      (settled-pipe (settle-pending pipe))
    )
    ;; Exit early if a forced closure is already in progress.
    (asserts! (is-none closer) ERR_CLOSE_IN_PROGRESS)

    ;; Exit early if there is a pending deposit
    (asserts!
      (and
        (is-none (get pending-1 settled-pipe))
        (is-none (get pending-2 settled-pipe))
      )
      ERR_PENDING
    )

    ;; Exit early if the nonce is less than the pipe's nonce
    (asserts! (> nonce pipe-nonce) ERR_NONCE_TOO_LOW)

    ;; Exit early if the transfer is not valid yet
    (match valid-after
      after (asserts! (<= after burn-block-height) ERR_NOT_VALID_YET)
      false
    )

    ;; If the total balance of the pipe is not equal to the sum of the
    ;; balances provided, the pipe close is invalid.
    (asserts!
      (is-eq (+ my-balance their-balance)
        (+ (get balance-1 settled-pipe) (get balance-2 settled-pipe))
      )
      ERR_INVALID_TOTAL_BALANCE
    )
    (let (
        (expires-at (+ burn-block-height WAITING_PERIOD))
        (signed-balances (map-balances tx-sender pipe-key my-balance their-balance))
        (balance-1 (get balance-1 signed-balances))
        (balance-2 (get balance-2 signed-balances))
        (new-pipe {
          balance-1: balance-1,
          balance-2: balance-2,
          pending-1: none,
          pending-2: none,
          expires-at: expires-at,
          closer: (some tx-sender),
          nonce: nonce,
        })
      )
      ;; Verify the signatures of the two parties.
      (try! (verify-signatures my-signature tx-sender their-signature with pipe-key
        balance-1 balance-2 nonce action actor secret valid-after
      ))

      ;; Set the waiting period for this pipe.
      (map-set pipes pipe-key new-pipe)

      ;; Emit an event
      (print {
        event: "force-close",
        pipe-key: pipe-key,
        pipe: new-pipe,
        sender: tx-sender,
      })

      (ok expires-at)
    )
  )
)

;;; Dispute the closing of a pipe that has been closed early by submitting a
;;; dispute within the waiting period. If the dispute is valid, the pipe
;;; will be closed and the new balances will be paid out to the appropriate
;;; parties.
;;; Returns:
;;; - `(ok false)` on success if the pipe's token was STX
;;; - `(ok true)` on success if the pipe's token was a SIP-010 token
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NO_CLOSE_IN_PROGRESS` if a forced closure is not in progress
;;; - `ERR_SELF_DISPUTE` if the sender is disputing their own force closure
;;; - `ERR_PIPE_EXPIRED` if the pipe has already expired
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the sum of the balances provided
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
;;; - `ERR_WITHDRAWAL_FAILED` if the withdrawal fails
(define-public (dispute-closure
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (dispute-closure-inner tx-sender token with my-balance their-balance
    my-signature their-signature nonce action actor secret valid-after
  )
)

;;; Dispute the closing of a pipe on behalf of `for` by submitting a dispute
;;; within the waiting period. This function is permissionless: any principal
;;; may submit valid signatures for `for`.
;;; Returns:
;;; - `(ok false)` on success if the pipe's token was STX
;;; - `(ok true)` on success if the pipe's token was a SIP-010 token
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NO_CLOSE_IN_PROGRESS` if a forced closure is not in progress
;;; - `ERR_SELF_DISPUTE` if the sender is disputing their own force closure
;;; - `ERR_PIPE_EXPIRED` if the pipe has already expired
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the sum of the balances provided
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
;;; - `ERR_WITHDRAWAL_FAILED` if the withdrawal fails
(define-public (dispute-closure-for
    (for principal)
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (dispute-closure-inner for token with my-balance their-balance my-signature
    their-signature nonce action actor secret valid-after
  )
)

;;; Close the pipe after a forced cancel or closure, once the required
;;; number of blocks have passed.
;;; Returns:
;;; - `(ok false)` on success if the pipe's token was STX
;;; - `(ok true)` on success if the pipe's token was a SIP-010 token
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NO_CLOSE_IN_PROGRESS` if a forced closure is not in progress
;;; - `ERR_NOT_EXPIRED` if the waiting period has not passed
;;; - `ERR_WITHDRAWAL_FAILED` if the withdrawal fails
(define-public (finalize
    (token (optional <sip-010>))
    (with principal)
  )
  (finalize-inner tx-sender token with)
)

;;; Finalize a pipe closure on behalf of `for`. This function is permissionless:
;;; anyone may finalize an expired closure.
(define-public (finalize-for
    (for principal)
    (token (optional <sip-010>))
    (with principal)
  )
  (finalize-inner for token with)
)

;;; Finalize a pipe closure for the pair (`for`, `with`), if expired.
(define-private (finalize-inner
    (for principal)
    (token (optional <sip-010>))
    (with principal)
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) for with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (closer (get closer pipe))
      (expires-at (get expires-at pipe))
    )
    ;; A forced closure must be in progress
    (asserts! (is-some closer) ERR_NO_CLOSE_IN_PROGRESS)

    ;; The waiting period must have passed
    (asserts! (>= burn-block-height expires-at) ERR_NOT_EXPIRED)

    ;; Reset the pipe in the map.
    (reset-pipe pipe-key (get nonce pipe))

    ;; Emit an event
    (print {
      event: "finalize",
      pipe-key: pipe-key,
      pipe: pipe,
      sender: tx-sender,
    })

    (payout token (get principal-1 pipe-key) (get principal-2 pipe-key)
      (get balance-1 pipe) (get balance-2 pipe)
    )
  )
)

;;; Deposit `amount` additional funds into an existing pipe between
;;; `tx-sender` and `with` for FT `token` (`none` indicates STX). Signatures
;;; must confirm the deposit and the new balances.
;;; Returns:
;;; - `(ok pipe-key)` on success
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_CLOSE_IN_PROGRESS` if a forced closure is in progress
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the sum of the balances provided and the deposit amount
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
;;; - `ERR_DEPOSIT_FAILED` if the deposit fails
(define-public (deposit
    (amount uint)
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (principal-1 (get principal-1 pipe-key))
      ;; These are the balances that both parties have signed off on, including
      ;; the deposit amount.
      (signed-balances (map-balances tx-sender pipe-key my-balance their-balance))
      (balance-1 (get balance-1 signed-balances))
      (balance-2 (get balance-2 signed-balances))
      (settled-pipe (settle-pending pipe))
      (pending-1-amount (match (get pending-1 settled-pipe)
        pending (get amount pending)
        u0
      ))
      (pending-2-amount (match (get pending-2 settled-pipe)
        pending (get amount pending)
        u0
      ))
    )
    ;; Validate the same transition constraints used by read-only verification.
    (try! (validate-transition pipe-key balance-1 balance-2 nonce ACTION_DEPOSIT
      tx-sender amount none
    ))

    (let (
        ;; These are the settled balances that actually exist in the pipe while
        ;; the deposit is pending.
        (pre-balance-1 (if (is-eq tx-sender principal-1)
          (- my-balance amount)
          (- their-balance pending-1-amount)
        ))
        (pre-balance-2 (if (is-eq tx-sender principal-1)
          (- their-balance pending-2-amount)
          (- my-balance amount)
        ))
        (updated-pipe (try! (increase-sender-balance pipe-key settled-pipe token amount)))
        (result-pipe (merge updated-pipe {
          balance-1: pre-balance-1,
          balance-2: pre-balance-2,
          nonce: nonce,
        }))
      )
      ;; Update the pipe with the new balances and nonce.
      (map-set pipes pipe-key result-pipe)

      ;; Verify the signatures of the two parties.
      (try! (verify-signatures my-signature tx-sender their-signature with pipe-key
        balance-1 balance-2 nonce ACTION_DEPOSIT tx-sender none none
      ))

      (print {
        event: "deposit",
        pipe-key: pipe-key,
        pipe: updated-pipe,
        sender: tx-sender,
        amount: amount,
        my-signature: my-signature,
        their-signature: their-signature,
      })

      (ok pipe-key)
    )
  )
)

;;; Withdrawal `amount` funds from an existing pipe between `tx-sender` and
;;; `with` for FT `token` (`none` indicates STX). Signatures must confirm the
;;; withdrawal and the new balances.
;;; Returns:
;;; - `(ok pipe-key)` on success
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_CLOSE_IN_PROGRESS` if a forced closure is in progress
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance of the pipe is not
;;;   equal to the prior total balance minus the withdrawal amount
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the sender's signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the other party's signature is invalid
;;; - `ERR_WITHDRAWAL_FAILED` if the withdrawal transfer fails
(define-public (withdraw
    (amount uint)
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) tx-sender with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (signed-balances (map-balances tx-sender pipe-key my-balance their-balance))
      (balance-1 (get balance-1 signed-balances))
      (balance-2 (get balance-2 signed-balances))
      ;; Settle any pending deposits that may be in progress
      (settled-pipe (settle-pending pipe))
      (updated-pipe (merge settled-pipe {
        balance-1: balance-1,
        balance-2: balance-2,
        nonce: nonce,
      }))
    )
    ;; Validate the same transition constraints used by read-only verification.
    (try! (validate-transition pipe-key balance-1 balance-2 nonce ACTION_WITHDRAWAL
      tx-sender amount none
    ))

    ;; Update the pipe with the new balances and nonce.
    (map-set pipes pipe-key updated-pipe)

    ;; Verify the signatures of the two parties.
    (try! (verify-signatures my-signature tx-sender their-signature with pipe-key
      balance-1 balance-2 nonce ACTION_WITHDRAWAL tx-sender none none
    ))

    ;; Perform the withdraw
    (try! (execute-withdraw token amount))

    (print {
      event: "withdraw",
      pipe-key: pipe-key,
      pipe: updated-pipe,
      sender: tx-sender,
      amount: amount,
      my-signature: my-signature,
      their-signature: their-signature,
    })

    (ok pipe-key)
  )
)

;; Read Only Functions
;;

;;; Get the current balances of the pipe between `tx-sender` and `with` for
;;; token `token` (`none` indicates STX).
;;; Returns:
;;; - The pipe data tuple on success, with type
;;;   ```
;;;   (some {
;;;     balance-1: uint,
;;;     balance-2: uint,
;;;     pending-1: (optional {
;;;       amount: uint,
;;;       burn-height: uint,
;;;     }),
;;;     pending-2: (optional {
;;;       amount: uint,
;;;       burn-height: uint,
;;;     }),
;;;     expires-at: uint,
;;;     nonce: uint,
;;;     closer: (optional principal)
;;;   })
;;;   ```
;;; - `none` if the pipe does not exist
(define-read-only (get-pipe
    (token (optional principal))
    (with principal)
  )
  (match (get-pipe-key token tx-sender with)
    pipe-key (map-get? pipes pipe-key)
    e none
  )
)

;;; Generate a hash of the structured data for a pipe.
;;; Returns:
;;; - (ok (buff 32)) with the hash of the structured data on success
;;; - `ERR_CONSENSUS_BUFF` if the structured data cannot be converted to a
;;;   consensus buff
(define-read-only (make-structured-data-hash
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (hashed-secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let (
      (structured-data (merge pipe-key {
        balance-1: balance-1,
        balance-2: balance-2,
        nonce: nonce,
        action: action,
        actor: actor,
        hashed-secret: hashed-secret,
        valid-after: valid-after,
      }))
      (data-hash (sha256 (unwrap! (to-consensus-buff? structured-data) ERR_CONSENSUS_BUFF)))
    )
    (ok (sha256 (concat structured-data-header data-hash)))
  )
)

;;; Validates that the specified data is valid for the pipe and that
;;; `signature` is a valid signature from `signer` for this data.
;;; Returns:
;;; - `(ok none)` if the signature is valid now
;;; - `(ok (some burn-block))` if the signature will be valid at `burn-block`
;;; - `ERR_CONSENSUS_BUFF` if the structured data cannot be converted to a
;;;   consensus buff
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance is invalid
;;; - `ERR_INVALID_BALANCES` if the balances would require spending pending
;;;   deposits
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the signature is invalid
(define-read-only (verify-signature
    (signature (buff 65))
    (signer principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (hashed-secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let (
      (hash (try! (make-structured-data-hash pipe-key balance-1 balance-2 nonce action actor
        hashed-secret valid-after
      )))
      (after (default-to burn-block-height valid-after))
    )
    (try! (balance-check pipe-key balance-1 balance-2 valid-after))
    (try! (nonce-check pipe-key nonce))
    (try! (verify-hash-signature hash signature signer))
    (if (> after burn-block-height)
      (ok (some (- after burn-block-height)))
      (ok none)
    )
  )
)

;;; Validates that the specified data is valid for the pipe and that
;;; `signature` is a valid signature from `signer` for this data with the
;;; provided `secret`.
;;; Returns:
;;; - `(ok none)` if the signature is valid now
;;; - `(ok (some burn-block))` if the signature will be valid at `burn-block`
;;; - `ERR_CONSENSUS_BUFF` if the structured data cannot be converted to a
;;;   consensus buff
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_NONCE_TOO_LOW` if the nonce is less than the pipe's saved nonce
;;; - `ERR_INVALID_TOTAL_BALANCE` if the total balance is invalid
;;; - `ERR_INVALID_BALANCES` if the balances would require spending pending
;;;   deposits
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the signature is invalid
(define-read-only (verify-signature-with-secret
    (signature (buff 65))
    (signer principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let ((hashed-secret (match secret
      s (some (sha256 s))
      none
    )))
    (verify-signature signature signer pipe-key balance-1 balance-2 nonce action
      actor hashed-secret valid-after
    )
  )
)

;;; Validates that the specified data is valid for the action and that
;;; `signature` is a valid signature from `signer` for this data.
;;; For `ACTION_DEPOSIT` and `ACTION_WITHDRAWAL`, `amount` is required to match
;;; the same balance equations enforced by the corresponding public functions.
;;; Returns:
;;; - `(ok none)` if the signature is valid now
;;; - `(ok (some burn-block))` if the signature will be valid at `burn-block`
;;; - Same error semantics as `verify-signature-with-secret`, with action-
;;;   specific balance checks for deposit and withdrawal.
(define-read-only (verify-signature-request
    (signature (buff 65))
    (signer principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
    (amount uint)
  )
  (let (
      (hashed-secret (match secret
        s (some (sha256 s))
        none
      ))
      (hash (try! (make-structured-data-hash pipe-key balance-1 balance-2 nonce action actor
        hashed-secret valid-after
      )))
      (after (default-to burn-block-height valid-after))
    )
    (try! (validate-transition pipe-key balance-1 balance-2 nonce action actor amount
      valid-after
    ))
    (try! (verify-hash-signature hash signature signer))
    (if (> after burn-block-height)
      (ok (some (- after burn-block-height)))
      (ok none)
    )
  )
)

;;; Validates that `signature-1` and `signature-2` are valid signature from
;;; `signer-1` and `signer-2`, respectively, for the structured data
;;; constructed from the other arguments.
;;; Returns:
;;; - `(ok true)` if both signatures are valid
;;; - `ERR_NO_SUCH_PIPE` if the pipe does not exist
;;; - `ERR_INVALID_SENDER_SIGNATURE` if the first signature is invalid
;;; - `ERR_INVALID_OTHER_SIGNATURE` if the second signature is invalid
(define-read-only (verify-signatures
    (signature-1 (buff 65))
    (signer-1 principal)
    (signature-2 (buff 65))
    (signer-2 principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let (
      (hashed-secret (match secret
        s (some (sha256 s))
        none
      ))
      (hash (try! (make-structured-data-hash pipe-key balance-1 balance-2 nonce action actor
        hashed-secret valid-after
      )))
    )
    (try! (balance-check pipe-key balance-1 balance-2 valid-after))
    (unwrap! (verify-hash-signature hash signature-1 signer-1)
      ERR_INVALID_SENDER_SIGNATURE
    )
    (unwrap! (verify-hash-signature hash signature-2 signer-2)
      ERR_INVALID_OTHER_SIGNATURE
    )
    (ok true)
  )
)

;; Private Functions
;;

;;; Given an optional trait, return an optional principal for the trait.
(define-private (contract-of-optional (trait (optional <sip-010>)))
  (match trait
    t (some (contract-of t))
    none
  )
)

;;; Given two principals, return the key for the pipe between these two principals.
;;; The key is a map with two keys: principal-1 and principal-2, where principal-1 is the principal
;;; with the lower consensus representation.
(define-private (get-pipe-key
    (token (optional principal))
    (principal-1 principal)
    (principal-2 principal)
  )
  (let (
      (p1 (unwrap! (to-consensus-buff? principal-1) ERR_INVALID_PRINCIPAL))
      (p2 (unwrap! (to-consensus-buff? principal-2) ERR_INVALID_PRINCIPAL))
    )
    (ok (if (< p1 p2)
      {
        token: token,
        principal-1: principal-1,
        principal-2: principal-2,
      }
      {
        token: token,
        principal-1: principal-2,
        principal-2: principal-1,
      }
    ))
  )
)

;;; Map caller-relative balances (`my-balance`, `their-balance`) into the
;;; canonical pipe ordering (`balance-1`, `balance-2`).
(define-private (map-balances
    (for principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (my-balance uint)
    (their-balance uint)
  )
  (let ((principal-1 (get principal-1 pipe-key)))
    {
      balance-1: (if (is-eq for principal-1) my-balance their-balance),
      balance-2: (if (is-eq for principal-1) their-balance my-balance),
    }
  )
)

;;; Transfer `amount` from `tx-sender` to the contract and update the pipe
;;; balances.
;;; Returns:
;;; - `(ok pipe)` on success, where `pipe` is the updated pipe data tuple
;;; - `ERR_DEPOSIT_FAILED` if the deposit fails
;;; - `ERR_ALREADY_PENDING` if there is already a pending deposit for the
;;;   sender
(define-private (increase-sender-balance
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (pipe {
      balance-1: uint,
      balance-2: uint,
      pending-1: (optional {
        amount: uint,
        burn-height: uint,
      }),
      pending-2: (optional {
        amount: uint,
        burn-height: uint,
      }),
      expires-at: uint,
      nonce: uint,
      closer: (optional principal),
    })
    (token (optional <sip-010>))
    (amount uint)
  )
  (begin
    (match token
      t (unwrap! (contract-call? t transfer amount tx-sender current-contract none)
        ERR_DEPOSIT_FAILED
      )
      (unwrap! (stx-transfer? amount tx-sender current-contract)
        ERR_DEPOSIT_FAILED
      )
    )
    (ok (if (is-eq tx-sender (get principal-1 pipe-key))
      (begin
        (asserts! (is-none (get pending-1 pipe)) ERR_ALREADY_PENDING)
        (merge pipe { pending-1: (some {
          amount: amount,
          burn-height: (+ burn-block-height CONFIRMATION_DEPTH),
        }) }
        )
      )
      (begin
        (asserts! (is-none (get pending-2 pipe)) ERR_ALREADY_PENDING)
        (merge pipe { pending-2: (some {
          amount: amount,
          burn-height: (+ burn-block-height CONFIRMATION_DEPTH),
        }) }
        )
      )
    ))
  )
)

;;; Transfer `amount` from the contract to `tx-sender`.
;;; Note that this function assumes that the token contract has already been
;;; verified (by finding the corresponding pipe).
(define-private (execute-withdraw
    (token (optional <sip-010>))
    (amount uint)
  )
  (let ((sender tx-sender))
    (unwrap!
      (match token
        t (as-contract? ((with-ft (contract-of t) "*" amount))
          (unwrap!
            (contract-call? t transfer amount current-contract sender none)
            ERR_WITHDRAWAL_FAILED
          ))
        (as-contract? ((with-stx amount))
          (unwrap! (stx-transfer? amount current-contract sender)
            ERR_WITHDRAWAL_FAILED
          ))
      )
      ERR_ALLOWANCE_VIOLATION
    )
    (ok true)
  )
)

;;; Inner function called by `dispute-closure` and `dispute-closure-for`.
(define-private (dispute-closure-inner
    (for principal)
    (token (optional <sip-010>))
    (with principal)
    (my-balance uint)
    (their-balance uint)
    (my-signature (buff 65))
    (their-signature (buff 65))
    (nonce uint)
    (action uint)
    (actor principal)
    (secret (optional (buff 32)))
    (valid-after (optional uint))
  )
  (let (
      (pipe-key (try! (get-pipe-key (contract-of-optional token) for with)))
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (expires-at (get expires-at pipe))
      (pipe-nonce (get nonce pipe))
      (closer (unwrap! (get closer pipe) ERR_NO_CLOSE_IN_PROGRESS))
      (signed-balances (map-balances for pipe-key my-balance their-balance))
      (balance-1 (get balance-1 signed-balances))
      (balance-2 (get balance-2 signed-balances))
    )
    ;; Exit early if this is an attempt to self-dispute
    (asserts! (not (is-eq for closer)) ERR_SELF_DISPUTE)

    ;; Exit early if the pipe has already expired
    (asserts! (< burn-block-height expires-at) ERR_PIPE_EXPIRED)

    ;; Exit early if the nonce is less than the pipe's nonce
    (asserts! (> nonce pipe-nonce) ERR_NONCE_TOO_LOW)

    ;; Exit early if the transfer is not valid yet
    (match valid-after
      after (asserts! (<= after burn-block-height) ERR_NOT_VALID_YET)
      false
    )

    ;; If the total balance of the pipe is not equal to the sum of the
    ;; balances provided, the pipe close is invalid.
    (asserts!
      (is-eq (+ my-balance their-balance)
        (+ (get balance-1 pipe) (get balance-2 pipe))
      )
      ERR_INVALID_TOTAL_BALANCE
    )
    (let ((updated-pipe {
        balance-1: balance-1,
        balance-2: balance-2,
        expires-at: MAX_HEIGHT,
        nonce: nonce,
        closer: none,
      }))
      ;; Verify the signatures of the two parties.
      (try! (verify-signatures my-signature for their-signature with pipe-key balance-1
        balance-2 nonce action actor secret valid-after
      ))

      ;; Reset the pipe in the map.
      (reset-pipe pipe-key nonce)

      ;; Emit an event
      (print {
        event: "dispute-closure",
        pipe-key: pipe-key,
        pipe: updated-pipe,
        sender: for,
      })

      ;; Pay out the balances.
      (payout token for with my-balance their-balance)
    )
  )
)

;;; Check if the balance of `account` in the pipe is greater than 0.
(define-private (is-funded
    (account principal)
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (pipe {
      balance-1: uint,
      balance-2: uint,
      pending-1: (optional {
        amount: uint,
        burn-height: uint,
      }),
      pending-2: (optional {
        amount: uint,
        burn-height: uint,
      }),
      expires-at: uint,
      nonce: uint,
      closer: (optional principal),
    })
  )
  (or
    (and (is-eq account (get principal-1 pipe-key)) (> (get balance-1 pipe) u0))
    (and (is-eq account (get principal-2 pipe-key)) (> (get balance-2 pipe) u0))
  )
)

;;; Payout the balances. Handles both SIP-010 tokens and STX.
;;; Returns `(ok true)` upons successfully paying out SIP-010 balances and
;;; `(ok false)` upon successfully paying out STX balances.
(define-private (payout
    (token (optional <sip-010>))
    (principal-1 principal)
    (principal-2 principal)
    (balance-1 uint)
    (balance-2 uint)
  )
  (begin
    (try! (transfer token principal-1 balance-1))
    (transfer token principal-2 balance-2)
  )
)

;;; Transfer `amount` of `token` to `addr`. Handles both SIP-010 tokens and STX.
(define-private (transfer
    (token (optional <sip-010>))
    (addr principal)
    (amount uint)
  )
  (if (is-eq amount u0)
    ;; Don't try to transfer 0, this will cause an error
    (ok (is-some token))
    (begin
      (unwrap!
        (match token
          t (as-contract? ((with-ft (contract-of t) "*" amount))
            (unwrap!
              (contract-call? t transfer amount current-contract addr none)
              ERR_WITHDRAWAL_FAILED
            ))
          (as-contract? ((with-stx amount))
            (unwrap! (stx-transfer? amount current-contract addr)
              ERR_WITHDRAWAL_FAILED
            ))
        )
        ERR_ALLOWANCE_VIOLATION
      )
      (ok (is-some token))
    )
  )
)

;;; Reset the pipe so that it is closed but retains the last nonce.
(define-private (reset-pipe
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (nonce uint)
  )
  (map-set pipes pipe-key {
    balance-1: u0,
    balance-2: u0,
    pending-1: none,
    pending-2: none,
    expires-at: MAX_HEIGHT,
    nonce: nonce,
    closer: none,
  })
)

;;; Verify a signature for a hash.
;;; Returns:
;;; - `(ok true)` if the signature is valid
;;; - `ERR_INVALID_SIGNATURE` if the signature is invalid
(define-private (verify-hash-signature
    (hash (buff 32))
    (signature (buff 65))
    (signer principal)
  )
  (let ((recovered (unwrap!
      (principal-of? (unwrap! (secp256k1-recover? hash signature) ERR_INVALID_SIGNATURE))
      ERR_INVALID_SIGNATURE
    )))
    (asserts!
      (or
        (is-eq recovered signer)
        ;; Contract principals may delegate signing to an agent key.
        (and
          (is-contract-principal signer)
          (match (map-get? agents signer)
            agent (is-eq recovered agent)
            false
          )
        )
      )
      ERR_INVALID_SIGNATURE
    )
    (ok true)
  )
)

;;; Determine whether a principal is a contract principal.
;;; Standard principals serialize to 22 bytes; contract principals include a
;;; contract-name suffix and therefore serialize to a longer buffer.
(define-private (is-contract-principal (p principal))
  (> (len (unwrap-panic (to-consensus-buff? p))) u22)
)

;;; Check that the contract has been initialized and `token` is the supported token.
(define-private (check-token (token (optional <sip-010>)))
  (begin
    ;; Ensure that the contract has been initialized
    (asserts! (var-get initialized) ERR_NOT_INITIALIZED)

    ;; Verify that this is the supported token
    (asserts! (is-eq (contract-of-optional token) (var-get supported-token))
      ERR_UNAPPROVED_TOKEN
    )

    (ok true)
  )
)

;;; Settle the pending deposit(s) for a pipe at the current burn height.
;;; Returns the updated pipe, without writing it to storage.
(define-private (settle-pending
    (pipe {
      balance-1: uint,
      balance-2: uint,
      pending-1: (optional {
        amount: uint,
        burn-height: uint,
      }),
      pending-2: (optional {
        amount: uint,
        burn-height: uint,
      }),
      expires-at: uint,
      nonce: uint,
      closer: (optional principal),
    })
  )
  (let (
      (settle-1 (match (get pending-1 pipe)
        pending (if (>= burn-block-height (get burn-height pending))
          {
            balance-1: (+ (get balance-1 pipe) (get amount pending)),
            pending-1: none,
          }
          {
            balance-1: (get balance-1 pipe),
            pending-1: (some pending),
          }
        )
        {
          balance-1: (get balance-1 pipe),
          pending-1: none,
        }
      ))
      (settle-2 (match (get pending-2 pipe)
        pending (if (>= burn-block-height (get burn-height pending))
          {
            balance-2: (+ (get balance-2 pipe) (get amount pending)),
            pending-2: none,
          }
          {
            balance-2: (get balance-2 pipe),
            pending-2: (some pending),
          }
        )
        {
          balance-2: (get balance-2 pipe),
          pending-2: none,
        }
      ))
      (updated-pipe (merge (merge pipe settle-1) settle-2))
    )
    updated-pipe
  )
)

;;; Compute confirmed and pending balances for each side at `at-height`.
(define-private (pipe-balance-state
    (pipe {
      balance-1: uint,
      balance-2: uint,
      pending-1: (optional {
        amount: uint,
        burn-height: uint,
      }),
      pending-2: (optional {
        amount: uint,
        burn-height: uint,
      }),
      expires-at: uint,
      nonce: uint,
      closer: (optional principal),
    })
    (at-height uint)
  )
  (let (
      (pipe-balances-1 (calculate-balances
        (get balance-1 pipe)
        (get pending-1 pipe)
        at-height
      ))
      (pipe-balances-2 (calculate-balances
        (get balance-2 pipe)
        (get pending-2 pipe)
        at-height
      ))
      (confirmed-1 (get confirmed pipe-balances-1))
      (confirmed-2 (get confirmed pipe-balances-2))
      (pending-1 (get pending pipe-balances-1))
      (pending-2 (get pending pipe-balances-2))
      (confirmed-total (+ confirmed-1 confirmed-2))
      (pending-total (+ pending-1 pending-2))
    )
    {
      confirmed-1: confirmed-1,
      confirmed-2: confirmed-2,
      pending-1: pending-1,
      pending-2: pending-2,
      confirmed-total: confirmed-total,
      pending-total: pending-total,
      total: (+ confirmed-total pending-total),
    }
  )
)

;;; Check that the balances provided are legal for the pipe. Each participant
;;; cannot have spent more than their balance, excluding pending deposits.
(define-private (balance-check
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (at-height-opt (optional uint))
  )
  (let (
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (at-height (default-to burn-block-height at-height-opt))
      (state (pipe-balance-state pipe at-height))
      (sum (+ balance-1 balance-2))
    )
    ;; The sum of the balances must be equal to the sum of the pipe balances
    ;; and the pending deposits.
    (asserts! (is-eq sum (get total state)) ERR_INVALID_TOTAL_BALANCE)

    ;; Ensure that these balances do not require spending the pending deposits.
    (asserts!
      (<= balance-1
        (+ (get confirmed-1 state) (get pending-1 state)
          (get confirmed-2 state)
        ))
      ERR_INVALID_BALANCES
    )
    (asserts!
      (<= balance-2
        (+ (get confirmed-2 state) (get pending-2 state)
          (get confirmed-1 state)
        ))
      ERR_INVALID_BALANCES
    )
    (ok true)
  )
)

;;; Check action-specific balance invariants for signature validation.
;;; - For `ACTION_DEPOSIT`, validates the same total-balance and pending rules
;;;   enforced by `deposit`.
;;; - For `ACTION_WITHDRAWAL`, validates the same total-balance rules enforced
;;;   by `withdraw`.
;;; - For `ACTION_CLOSE`, validates the same pending and total-balance rules
;;;   enforced by `close-pipe`.
;;; - For other actions, defers to `balance-check`.
(define-private (action-balance-check
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (action uint)
    (actor principal)
    (amount uint)
    (at-height-opt (optional uint))
  )
  (let (
      (pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE))
      (at-height (default-to burn-block-height at-height-opt))
      (state (pipe-balance-state pipe at-height))
      (sum (+ balance-1 balance-2))
      (principal-1 (get principal-1 pipe-key))
      (principal-2 (get principal-2 pipe-key))
      (actor-balance (if (is-eq actor principal-1)
        balance-1
        balance-2
      ))
      (actor-pending (if (is-eq actor principal-1)
        (get pending-1 state)
        (get pending-2 state)
      ))
    )
    (if (is-eq action ACTION_DEPOSIT)
      (begin
        (asserts! (or (is-eq actor principal-1) (is-eq actor principal-2))
          ERR_INVALID_PRINCIPAL
        )
        (asserts! (is-none (get closer pipe)) ERR_CLOSE_IN_PROGRESS)
        (asserts! (>= actor-balance amount) ERR_INVALID_BALANCES)
        (asserts! (is-eq actor-pending u0) ERR_ALREADY_PENDING)
        (asserts! (is-eq sum (+ (get total state) amount)) ERR_INVALID_TOTAL_BALANCE)
        (ok true)
      )
      (if (is-eq action ACTION_WITHDRAWAL)
        (begin
          (asserts! (or (is-eq actor principal-1) (is-eq actor principal-2))
            ERR_INVALID_PRINCIPAL
          )
          (asserts! (is-none (get closer pipe)) ERR_CLOSE_IN_PROGRESS)
          (asserts! (>= (get confirmed-total state) amount) ERR_INVALID_WITHDRAWAL)
          (asserts! (is-eq sum (- (get confirmed-total state) amount)) ERR_INVALID_TOTAL_BALANCE)
          (ok true)
        )
        (if (is-eq action ACTION_CLOSE)
          (begin
            (asserts! (is-eq (get pending-total state) u0) ERR_PENDING)
            (asserts! (is-eq sum (get confirmed-total state)) ERR_INVALID_TOTAL_BALANCE)
            (ok true)
          )
          (balance-check pipe-key balance-1 balance-2 at-height-opt)
        )
      )
    )
  )
)

;;; Shared transition validation used by both public state-changing functions
;;; and read-only signature verification.
(define-private (validate-transition
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (balance-1 uint)
    (balance-2 uint)
    (nonce uint)
    (action uint)
    (actor principal)
    (amount uint)
    (at-height-opt (optional uint))
  )
  (begin
    (try! (nonce-check pipe-key nonce))
    (action-balance-check pipe-key balance-1 balance-2 action actor amount
      at-height-opt
    )
  )
)

;;; Given the current confirmed balance and an optional pending deposit,
;;; calculate the confirmed and pending balances at the current block height.
;;; Returns a tuple with the confirmed balance and the pending balance.
(define-private (calculate-balances
    (confirmed uint)
    (maybe-pending (optional {
      amount: uint,
      burn-height: uint,
    }))
    (at-height uint)
  )
  (match maybe-pending
    pending (if (>= at-height (get burn-height pending))
      {
        confirmed: (+ confirmed (get amount pending)),
        pending: u0,
      }
      {
        confirmed: confirmed,
        pending: (get amount pending),
      }
    )
    {
      confirmed: confirmed,
      pending: u0,
    }
  )
)

;;; Check that the nonce is valid for the pipe.
(define-private (nonce-check
    (pipe-key {
      token: (optional principal),
      principal-1: principal,
      principal-2: principal,
    })
    (nonce uint)
  )
  (let ((pipe (unwrap! (map-get? pipes pipe-key) ERR_NO_SUCH_PIPE)))
    ;; Nonce must be greater than the pipe nonce
    (asserts! (> nonce (get nonce pipe)) ERR_NONCE_TOO_LOW)
    (ok true)
  )
)

Functions (29)

FunctionAccessArgs
initpublictoken: (optional <sip-010>
register-agentpublicagent: principal
deregister-agentpublic
fund-pipepublictoken: (optional <sip-010>
close-pipepublictoken: (optional <sip-010>
force-cancelpublictoken: (optional <sip-010>
force-closepublictoken: (optional <sip-010>
dispute-closurepublictoken: (optional <sip-010>
dispute-closure-forpublicfor: principal, token: (optional <sip-010>
finalizepublictoken: (optional <sip-010>
finalize-forpublicfor: principal, token: (optional <sip-010>
finalize-innerprivatefor: principal, token: (optional <sip-010>
depositpublicamount: uint, token: (optional <sip-010>
withdrawpublicamount: uint, token: (optional <sip-010>
get-piperead-onlytoken: (optional principal
verify-signatureread-onlysignature: (buff 65
verify-signature-with-secretread-onlysignature: (buff 65
verify-signature-requestread-onlysignature: (buff 65
verify-signaturesread-onlysignature-1: (buff 65
contract-of-optionalprivatetrait: (optional <sip-010>
get-pipe-keyprivatetoken: (optional principal
execute-withdrawprivatetoken: (optional <sip-010>
dispute-closure-innerprivatefor: principal, token: (optional <sip-010>
payoutprivatetoken: (optional <sip-010>
transferprivatetoken: (optional <sip-010>
verify-hash-signatureprivatehash: (buff 32
is-contract-principalprivatep: principal
check-tokenprivatetoken: (optional <sip-010>
calculate-balancesprivateconfirmed: uint, maybe-pending: (optional { amount: uint, burn-height: uint, }