Files
nick-doc/04 - Flows/Escrow Flow.md

227 lines
10 KiB
Markdown

---
title: Escrow Flow
tags: [flow, escrow, payment, state-machine, custody]
related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[Funds Ledger and Escrow State Machine Specification]]"]
related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/refund", "POST /api/payment/:id/release/confirm", "POST /api/payment/:id/refund/confirm"]
---
# Escrow Flow
The current escrow is a **hybrid custody system**, not a custom Solidity escrow contract.
Buyer funds move on-chain through Request Network-compatible wallet transactions. The backend verifies the payment through signed Request Network webhooks/reconciliation plus the Transaction Safety Provider, records state in `Payment`, and records money movement in the internal funds ledger. Release/refund/sweep actions are still administered by the platform, with optional Trezor proof today and a recommended move to Safe multisig custody in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
## Actors
- **Buyer** -- pays from their wallet and confirms delivery.
- **Seller** -- fulfills the order and receives release.
- **Admin / mediator** -- resolves disputes and initiates release/refund when manual action is required.
- **Custody signer** -- Trezor today when enabled; target state is Safe multisig owners.
- **Request Network** -- emits payment evidence through signed webhooks and status APIs.
- **Transaction Safety Provider** -- verifies tx hash, confirmations, recipient, token, amount, and optional AML decision before funds are credited.
- **MongoDB** -- stores `Payment`, `FundsLedgerEntry`, `Dispute`, and `PurchaseRequest` state.
## Current State Model
`Payment.status` remains the coarse provider/business state:
- `pending`
- `processing`
- `confirmed`
- `completed`
- `failed`
- `cancelled`
- `refunded`
`Payment.escrowState` currently supports:
- `funded`
- `releasable`
- `releasing`
- `released`
- `refunded`
- `failed`
- `cancelled`
- `partial`
The current model also has `Payment.disputed`, `disputeHoldReason`, and `holdUntil`. The canonical target state machine in [[Funds Ledger and Escrow State Machine Specification]] adds explicit `DISPUTED`, `REFUNDING`, and normalized uppercase enums. Treat that spec as the destination; this page describes the live hybrid implementation.
```mermaid
stateDiagram-v2
[*] --> Pending : payment intent created
Pending --> Processing : funds detected / webhook received
Pending --> Cancelled : intent expired or buyer cancels
Processing --> Funded : Transaction Safety Provider approved
Processing --> Failed : verification rejected
Funded --> Releasable : delivery confirmed / release authorized
Funded --> DisputeHold : dispute opened
Releasable --> DisputeHold : dispute opened before payout
DisputeHold --> Funded : dispute rejected / no financial action
DisputeHold --> Releasable : resolved for seller
DisputeHold --> Refunding : resolved for buyer
Releasable --> Releasing : release instruction built
Releasing --> Released : tx hash confirmed
Releasing --> Failed : payout failed
Refunding --> Refunded : refund tx hash confirmed
Refunding --> Failed : refund failed
Failed --> Releasing : admin retries release
Failed --> Refunding : admin retries refund
Released --> [*]
Refunded --> [*]
Cancelled --> [*]
```
## Step-by-step Narrative
### 1. Funding
1. Buyer accepts a seller offer and starts Request Network checkout.
2. Backend creates a `Payment` and Request Network intent through `requestNetworkPayInService.ts`.
3. When configured, `getDestinationFor({ buyerId, sellerOfferId, chainId })` assigns a per-payment derived destination and stores it in `payment.metadata.derivedDestination`.
4. Frontend renders the in-house checkout block and the buyer signs RN-compatible on-chain transactions from their wallet.
5. Request Network webhook or reconciliation reports payment evidence.
6. The Transaction Safety Provider verifies:
- transaction hash exists,
- chain confirmations meet the runtime/env threshold,
- token, recipient, and amount match,
- AML/sanctions provider result when configured.
7. Only after safety approval does the backend mark the payment funded and append ledger entries.
### 2. Holding
While escrow is funded, funds are represented in two places:
- **On chain:** in the derived destination or custody wallet until swept/released/refunded.
- **In app accounting:** in `FundsLedgerEntry` rows and `Payment.escrowState`.
Release/refund eligibility must be derived from ledger availability, not raw mutable `Payment.status` alone. In production the roadmap requires `PAYMENT_LEDGER_ENFORCEMENT=true` before custody decentralization.
### 3. Release
Release is triggered by delivery confirmation, auto-release policy, or dispute resolution for the seller.
1. Admin calls `POST /api/payment/:id/release`.
2. Backend loads the payment and validates ledger availability when enforcement is enabled.
3. Backend builds a provider payment instruction.
4. Custody signer executes the transaction:
- current optional control: Trezor proof when `TREZOR_SAFEKEEPING_REQUIRED=true`;
- roadmap control: Safe multisig transaction proposal/execution.
5. Admin confirms with `POST /api/payment/:id/release/confirm` and tx hash.
6. Backend validates Trezor proof when required, confirms adapter state, and appends a `release` ledger entry.
### 4. Refund
Refund follows the same instruction/confirmation pattern as release, but destination is the buyer/refund wallet and ledger entry type is `refund`.
Refund can be triggered by dispute resolution for the buyer, pre-fulfillment cancellation, or an admin/manual recovery flow. A refund during an active dispute must be an explicit resolution path, not an accidental bypass.
### 5. Dispute Hold
Opening a dispute now has backend support through `releaseHoldService.ts`: it sets hold fields on the related purchase request and payments, and release/refund gates consult those holds.
Remaining alignment work:
- migrate from legacy dispute status enum to the canonical spec,
- make financial side effects automatic from final dispute resolution,
- ensure every release/refund path calls the same policy service,
- record immutable audit entries for dispute resolution and custody execution.
## Sequence Diagram - Funding
```mermaid
sequenceDiagram
autonumber
actor B as Buyer
participant FE as Frontend
participant BE as Backend
participant RN as Request Network
participant BC as EVM Chain
participant DB as MongoDB
B->>FE: Start Request Network checkout
FE->>BE: POST /api/payment/request-network/intents
BE->>DB: Payment.create(status="pending")
BE->>BE: Assign derived destination when configured
BE->>RN: Create Request Network intent
BE-->>FE: inHouseCheckout block
B->>BC: approve + transferFromWithReferenceAndFee
RN-->>BE: signed webhook / status evidence
BE->>BE: Transaction Safety Provider checks
BE->>DB: Payment.status="completed", escrowState="funded"
BE->>DB: append FundsLedgerEntry(payment_detected / hold)
```
## Sequence Diagram - Release / Refund
```mermaid
sequenceDiagram
autonumber
actor A as Admin
actor C as Custody signer
participant BE as Backend
participant DB as MongoDB
participant BC as EVM Chain
A->>BE: POST /api/payment/{id}/release or refund
BE->>DB: Load Payment + ledger balance
BE->>BE: Check dispute hold + ledger availability
BE-->>A: unsigned instruction
A->>C: Request signature / Safe execution
C->>BC: Broadcast tx
BC-->>C: txHash
A->>BE: POST /confirm { txHash, optional trezor proof }
BE->>BE: Verify signer proof when required
BE->>DB: append release/refund ledger entry
BE->>DB: escrowState="released" or "refunded"
```
## API Calls
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/api/payment/request-network/intents` | Create Request Network pay-in intent |
| `GET` | `/api/payment/request-network/:paymentId/checkout` | Rehydrate in-house checkout block |
| `POST` | `/api/payment/request-network/webhook` | Receive signed RN webhook |
| `POST` | `/api/payment/:id/release` | Build release instruction |
| `POST` | `/api/payment/:id/release/confirm` | Confirm release tx hash / signer proof |
| `POST` | `/api/payment/:id/refund` | Build refund instruction |
| `POST` | `/api/payment/:id/refund/confirm` | Confirm refund tx hash / signer proof |
| `GET` | `/api/payment/:id` | Read payment details |
| `GET` | `/api/payment/derived-destinations` | Admin list of derived destinations |
## Side Effects And Risks
- **No custom on-chain escrow contract yet.** This is deliberate; [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] recommends Safe/Trezor custody controls before a custom contract pilot.
- **Ledger enforcement is configurable.** `PAYMENT_LEDGER_ENFORCEMENT` must be enabled before real custody decentralization work is considered complete.
- **Trezor enforcement is configurable.** `TREZOR_SAFEKEEPING_REQUIRED=true` makes Trezor proof mandatory for release/refund confirmation, but target custody should be Safe multisig.
- **Durable webhook ingress is still roadmap work.** Until the Worker/replay layer is live, backend availability remains important for Request Network webhook delivery.
- **Dispute model is implemented but not fully canonical.** The current model works with legacy enum names; canonical status alignment remains required.
## Linked Flows
- [[PRD - Request Network In-House Checkout]] -- current primary pay-in path.
- [[Dispute Flow]] -- can block or redirect escrow.
- [[Delivery Confirmation Flow]] -- happy-path release trigger.
- [[Payout Flow]] -- historical payout context and release mechanics.
- [[Trezor Safekeeping Flow]] -- hardware proof for admin actions.
- [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] -- custody decentralization and smart-contract decision plan.
## Source Files
- Backend: `backend/src/models/Payment.ts`
- Backend: `backend/src/models/FundsLedgerEntry.ts`
- Backend: `backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts`
- Backend: `backend/src/services/payment/safety/transactionSafetyProvider.ts`
- Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts`
- Backend: `backend/src/services/payment/wallets/derivedDestinations.ts`
- Backend: `backend/src/services/payment/wallets/sweepService.ts`
- Backend: `backend/src/services/dispute/releaseHoldService.ts`
- Backend: `backend/src/services/trezor/trezorService.ts`