Initial commit: nick docs

This commit is contained in:
moojttaba
2026-05-23 20:35:34 +03:30
commit 0da235ae27
90 changed files with 18268 additions and 0 deletions

196
04 - Flows/Escrow Flow.md Normal file
View File

@@ -0,0 +1,196 @@
---
title: Escrow Flow
tags: [flow, escrow, payment, state-machine]
related_models: ["[[Payment]]", "[[PurchaseRequest]]"]
related_apis: ["POST /api/payment/release/:paymentId", "POST /api/payment/refund/:paymentId"]
---
# Escrow Flow
The escrow is not a separate smart contract — it is a **state machine on the `Payment` document** combined with a **custodial wallet** (the platform-controlled BSC address `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS`). Funds sit at that wallet once SHKeeper / Web3 verification completes, and are released to the seller or refunded to the buyer based on order outcome.
## Actors
- **System** — the backend, on receiving pay-in confirmation.
- **Buyer** — confirms delivery to authorise release; can open a dispute to block release.
- **Seller** — recipient of release.
- **Admin** — resolves disputes and signs payout transactions when manual control is required.
- **MongoDB** — `payments` document holds the canonical `escrowState`.
## Escrow state machine (`Payment.escrowState`)
Enum from `Payment.ts:112-115`: `funded | releasable | released | refunded | releasing | failed`.
```mermaid
stateDiagram-v2
[*] --> Pending: Payment.status="pending"\nescrowState=undefined
Pending --> Partial: webhook PARTIAL\nescrowState="partial"
Pending --> Funded: webhook PAID/OVERPAID\nor on-chain verify success\nescrowState="funded"
Partial --> Funded: top-up reaches threshold
Funded --> Releasable: buyer confirms delivery\n(or auto-release timer)
Releasable --> Releasing: admin/system initiates payout\n[[Payout Flow]]
Releasing --> Released: payout tx confirmed\nescrowState="released"
Releasing --> Failed: payout tx reverted\nescrowState="failed"
Funded --> Refunded: dispute resolution = refund\nescrowState="refunded"
Funded --> Refunded: order cancelled\npre-shipment
Failed --> Releasing: admin retries
Released --> [*]
Refunded --> [*]
```
`Payment.status` mirrors a coarser business state:
- `pending` → invoice issued, awaiting funds.
- `processing` → SHKeeper sees partial / confirmations in progress.
- `confirmed` → fully credited (intermediate; sometimes skipped).
- `completed` → escrow `funded` and onward.
- `failed`, `cancelled`, `refunded` → terminal.
## Step-by-step narrative
### 1. Funding
- Triggered by either [[Payment Flow - SHKeeper]] (webhook `PAID`/`OVERPAID`) or [[Payment Flow - DePay & Web3]] (verified `eth_getTransactionReceipt`).
- Backend sets `Payment.status = "completed"` and `Payment.escrowState = "funded"` (`shkeeperWebhook.ts:388-391`, `shkeeperService.ts:600-602`).
- Cascade: `PurchaseRequest.status``payment`, then `processing` once the seller acknowledges; `SellerOffer.status``accepted`; chat created.
- Funds physically sit at the **custodial wallet** — SHKeeper's per-invoice deposit address (auto-swept to the merchant wallet) or directly at the escrow wallet in the Web3 path.
### 2. Holding
- While `escrowState === "funded"` and the order is in `processing` / `delivery`, the funds are inert. No interest accrues; no on-chain action happens.
- The buyer cannot withdraw; the seller cannot collect. Only an admin/system action moves it forward.
- Visible in admin dashboard: `GET /api/payment/admin/funded?status=funded` (or similar — see admin payment view in `frontend/src/sections/payment/view/payment-list-admin-view.tsx`).
### 3. Releasing (happy path)
- Trigger options:
- **Buyer confirms delivery** via the delivery-code flow ([[Delivery Confirmation Flow]]).
- **Auto-release timer** elapses (configurable; today a manual or scheduled job — `PurchaseRequestService` exposes status transitions through to `completed`).
- **Admin manual release** from the admin payment detail view.
- The system marks `Payment.escrowState = "releasable"` (intermediate).
- `shkeeperPayoutService.createPayoutTask` (or a manual EVM admin signature via `admin-wallet-payout.tsx`) starts the on-chain transfer to the seller's verified wallet address. State flips to `releasing`.
- On confirmation: `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets:
- `Payment.status = 'completed'`
- `Payment.escrowState = 'released'`
- `Payment.blockchain.transactionHash = <payout tx hash>`
- Cascade: `PurchaseRequest.status``seller_paid` then `completed`.
### 4. Refunding (dispute / cancellation)
- Trigger: dispute resolution with `action: 'refund'` or pre-shipment cancellation.
- Backend builds the refund tx via `buildAdminSignedTxPayload(paymentId, 'refund')` (`shkeeperService.ts:614-626`) — destination is `payment.blockchain.sender` (the buyer's verified wallet).
- Admin signs and broadcasts (currently a manual step in the admin UI).
- On confirmation: `confirmAdminTx(paymentId, txHash, 'refund')` sets:
- `Payment.status = 'refunded'`
- `Payment.escrowState = 'refunded'`
- Cascade: `PurchaseRequest.status``cancelled` (or remains in dispute-resolved state).
### 5. Failed payout
- If the payout tx reverts (insufficient gas, contract pause, wrong address), `escrowState = 'failed'`. Admin can retry by initiating a fresh payout.
## Sequence diagram (release path)
```mermaid
sequenceDiagram
autonumber
actor B as Buyer
actor A as Admin
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant SK as SHKeeper Payout API
participant BC as BSC
B->>FE: Enter delivery code (or auto-timer fires)
FE->>BE: POST /api/marketplace/purchase-requests/:id/confirm-delivery
BE->>DB: PurchaseRequest.status="delivered"\nPayment.escrowState="releasable"
BE-->>FE: ok
A->>FE: Click "Release" in admin
FE->>BE: POST /api/payment/shkeeper/payout
BE->>DB: Payment.escrowState="releasing"
BE->>SK: createPayoutTask({recipient, amount})
SK->>BC: signed payout tx
BC-->>SK: confirmed
SK->>BE: payout webhook / poll
BE->>BE: confirmAdminTx(paymentId, txHash, "release")
BE->>DB: Payment.escrowState="released"\nPurchaseRequest.status="completed"
```
## Sequence diagram (refund path)
```mermaid
sequenceDiagram
autonumber
actor A as Admin
participant BE as Backend
participant DB as MongoDB
participant BC as BSC
actor B as Buyer
A->>BE: Dispute resolved with action="refund"
BE->>BE: buildAdminSignedTxPayload(paymentId, "refund")
BE-->>A: { to:buyerWallet, amount, token, network }
A->>BC: sign + broadcast tx
BC-->>A: txHash
A->>BE: confirmAdminTx(paymentId, txHash, "refund")
BE->>DB: Payment.status="refunded"\nescrowState="refunded"
BE->>B: notifyRefundCompleted
```
## API calls
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/api/payment/admin/release/:paymentId` | Initiate release |
| `POST` | `/api/payment/admin/refund/:paymentId` | Initiate refund |
| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Admin marks the signed tx confirmed |
| `GET` | `/api/payment/:paymentId/status` | Polled by both parties |
## Database writes
- **`payments`**: `status`, `escrowState`, `blockchain.transactionHash`, `completedAt`, `metadata.*` are mutated as the state progresses.
- **`purchaserequests`**: `status` cascades (`payment → processing → delivery → delivered → confirming → seller_paid → completed`).
- **`notifications`**: created on each terminal state.
## Socket events emitted
- **`purchase-request-update`** `status-changed` on every cascading status flip.
- **`payment-status`** (planned/admin) — admin dashboard real-time feed.
## Side effects
- **Custodial risk** — the escrow wallet's private key sits with the platform. Lose it → lose all in-flight escrows. Operational controls: hardware wallet, multi-sig, cold storage of the recovery seed.
- **No on-chain escrow contract** — there is no Solidity escrow today. Migration toward a smart-contract escrow (e.g. OpenZeppelin's `Escrow.sol` pattern) would remove custodial trust at the cost of higher complexity and gas.
## Error / edge cases
- **Buyer never confirms delivery** → today requires admin intervention. An auto-release timer (e.g. 7 days after `delivered`) is a recommended addition.
- **Seller's wallet address invalid** → payout tx fails or sends to a black hole. Validate `recipientAddress` shape (`^0x[0-9a-fA-F]{40}$`) before signing (`shkeeperPayoutService.ts:62-64` checks `.startsWith('0x')`).
- **Partial payment** (`PARTIAL`) → escrow remains in `pending/partial`; release blocked until full payment arrives.
- **Overpaid** → currently treated as `completed/funded`; the surplus is not auto-refunded.
- **Concurrent release + refund** → blocked by `PaymentCoordinator` serialisation; whichever fires first wins, the other is rejected.
- **Payout fails on chain** → state stays in `releasing` until admin re-runs; consider auto-retry with exponential backoff.
- **Disputed payment** → `escrowState` is **not** auto-changed when a dispute is opened. Admin must explicitly resolve to refund/release. Add a `disputed` boolean or `escrowState='disputed'` to make this more obvious.
> [!warning] Single custodial wallet = single point of failure
> Centralising all in-flight escrow at one BSC address is the platform's largest operational risk. Use a multi-sig (Gnosis Safe) for the escrow wallet, store one key in HSM, and require two admin signatures for any payout > a threshold.
> [!tip] Recovering inconsistent state
> If `Payment.escrowState` looks stale (e.g. `released` but no on-chain tx hash), inspect with `Payment.find({ escrowState: 'released', 'blockchain.transactionHash': { $exists: false } })` and reconcile via the SHKeeper invoice or the `fix-transaction-hashes.js` script.
## Linked flows
- [[Payment Flow - SHKeeper]] — funds the escrow.
- [[Payment Flow - DePay & Web3]] — alternative funding path.
- [[Delivery Confirmation Flow]] — triggers release.
- [[Dispute Flow]] — can divert to refund.
- [[Payout Flow]] — executes the release transfer.
## Source files
- Backend: `backend/src/models/Payment.ts:96-145` (status + escrowState enums)
- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:600-647`
- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:387-411`
- Backend: `backend/src/services/payment/paymentCoordinator.ts`
- Frontend: `frontend/src/sections/payment/view/payment-list-admin-view.tsx`
- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx`