227 lines
10 KiB
Markdown
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`
|