Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
244 lines
18 KiB
Markdown
244 lines
18 KiB
Markdown
---
|
|
title: Payment Flow - DePay & Web3
|
|
tags: [flow, payment, web3, wagmi, walletconnect, bsc]
|
|
related_models: ["[[Payment]]", "[[PurchaseRequest]]"]
|
|
related_apis: ["POST /api/payment/decentralized/save", "POST /api/payment/decentralized/verify/:paymentId"]
|
|
---
|
|
|
|
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
|
|
|
> [!caution] Audit — 2026-05-29
|
|
> This document was reviewed against the live codebase. **12 corrections applied** — endpoint paths, missing route bugs, TypeScript type gaps, a security issue, and stats undercounting. See inline ⚠️ callouts throughout.
|
|
|
|
# Payment Flow — DePay & Web3 (Wallet-Direct)
|
|
|
|
> [!warning] Historical/legacy path
|
|
> This page describes the older wallet-direct payment path. The current primary checkout is [[PRD - Request Network In-House Checkout]] with Request Network metadata, derived destinations, and Transaction Safety Provider checks. Keep this page for migration and verification context only.
|
|
|
|
Legacy alternative pay-in path: the buyer connects their own wallet (MetaMask / WalletConnect / Coinbase Wallet) and signs an **on-chain transfer to the escrow wallet** directly. The backend then verifies the transaction against the BSC RPC.
|
|
|
|
## Actors
|
|
|
|
- **Buyer** — owner of the wallet doing the on-chain transfer.
|
|
- **Frontend** — `frontend/src/web3/` (wagmi provider, web3-provider context); wallet-connect UI in `frontend/src/sections/account/account-wallet-connection.tsx`; the in-checkout integration is in `frontend/src/sections/request/components/buyer-steps/payment-card.tsx` and `frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx`.
|
|
- **Wagmi / WalletConnect / MetaMask** — wallet stack.
|
|
- **Backend** — `decentralizedPaymentService.ts` (intent), `BSCTransactionVerifier` (on-chain verification), `decentralizedPaymentRoutes.ts`.
|
|
- **Blockchain (BSC)** — verified via `https://bsc-dataseed.binance.org/` JSON-RPC.
|
|
- **MongoDB** — `payments` collection, with `provider` distinguishing the legacy wallet-direct source from Request Network.
|
|
- **Socket.IO** — `payment-created`, plus the funded-escrow cascade events when verification succeeds.
|
|
|
|
## Preconditions
|
|
|
|
- `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` is set on the frontend (see `frontend/src/global-config.ts`). This is the destination address — typically a custodial BSC wallet operated by the platform admin.
|
|
- Wagmi is configured with WalletConnect projectID and supported chains (BSC mainnet `56`, optionally BSC testnet `97`).
|
|
- Buyer has a wallet with USDT/USDC balance and a small amount of BNB for gas.
|
|
|
|
## Step-by-step narrative
|
|
|
|
### Phase 1 — Connect wallet
|
|
|
|
1. Buyer hits the payment step and sees both pay options. Clicking **"Pay with wallet"** opens the WalletConnect modal via wagmi's `useConnect()`.
|
|
2. The connection emits an `accountsChanged` event; the web3 context (`frontend/src/web3/context/web3-provider.tsx`) stores `wallet.address` and `wallet.chainId`.
|
|
3. If `chainId !== 56` (BSC), the UI prompts a `wallet_switchEthereumChain` request.
|
|
|
|
> [!warning] ⚠️ SECURITY: SIM_ bypass has no environment guard
|
|
> `web3-provider.tsx` generates `SIM_`-prefixed transaction hashes on wallet connection failure with **no `process.env.NODE_ENV` check**. In production, if a wallet connection fails, a `SIM_` hash can be submitted to the verify endpoint and may bypass on-chain verification checks. An explicit `if (process.env.NODE_ENV === 'production') throw` guard is required before generating simulation hashes.
|
|
|
|
### Phase 2 — Create intent on backend
|
|
|
|
4. Frontend POSTs `POST /api/payment/decentralized/save` with `{ purchaseRequestId, sellerOfferId, amount, fromAddress: wallet.address, token: 'USDT', network: 'bsc' }`. The backend records a `Payment` with `provider: 'other'` (or `'decentralized'` — see TypeScript type note below), `direction: 'in'`, `status: 'pending'`, `blockchain.{network, token, sender, receiver: ESCROW_WALLET_ADDRESS}`. **Auth:** Bearer JWT required.
|
|
|
|
> [!warning] ⚠️ TypeScript type gap — `PaymentProvider`
|
|
> The frontend `PaymentProvider` type is defined as `'request.network' | 'test' | 'other'`. The values **`'shkeeper'`** and **`'decentralized'`** are missing from the union. Any UI provider-switch logic that branches on `provider` will fall through to an unknown/default state for these two providers. Add both to the type definition.
|
|
|
|
5. Response includes the **escrow wallet address** and the exact token amount (in decimals — for USDT-BEP20 that's 18 decimals; the helper `convertPaymentAmountForShkeeper` is shared from `currencyUtils.ts`).
|
|
|
|
> [!warning] ⚠️ NOT IMPLEMENTED — `createDePayIntent()`
|
|
> The frontend action `createDePayIntent()` POSTs to `/payment/depay/intents`, which **does not exist** on the backend. Calling this action will always return 404. The working intent endpoint is `POST /api/payment/decentralized/save` (step 4 above). Do not use `createDePayIntent()` until a `/payment/depay/intents` route is added to the backend.
|
|
|
|
> [!warning] ⚠️ KNOWN BUG — `getProviderIntentEndpoint()` routing
|
|
> The `getProviderIntentEndpoint()` factory function **always** resolves to `/payment/request-network/intents` regardless of the `provider` argument passed in. Any SHKeeper checkout that calls this helper will POST to the wrong (Request Network) intent endpoint. This function requires a proper `switch`/`if` on `provider` before it can be used for non-Request-Network flows.
|
|
|
|
### Phase 3 — Token approval (ERC-20 / BEP-20)
|
|
|
|
6. The frontend checks the user's current allowance via `useReadContract` / `allowance(owner, spender)` on the USDT contract.
|
|
7. If allowance < amount, the frontend prompts an `approve(escrow, amount)` transaction. After confirmation, it proceeds to step 8.
|
|
- Some flows skip `approve` and use **direct transfer** instead (`transfer(to, amount)` — no approval needed because the buyer is the holder, not a contract pulling from them). The codebase favours direct transfer; see usages of `web3Service.transferToken(...)` in `frontend/src/web3/web3Service.ts`.
|
|
|
|
### Phase 4 — On-chain transfer
|
|
|
|
8. Frontend calls `transfer(escrowAddress, amount)` on the USDT contract via wagmi's `useWriteContract`. The wallet popup asks the user to confirm gas.
|
|
9. The buyer signs; the transaction is broadcast.
|
|
10. Frontend listens via wagmi's `useWaitForTransactionReceipt({ hash })`. On `success` (1 confirmation), it captures the `transactionHash`.
|
|
|
|
### Phase 5 — Backend verification
|
|
|
|
11. Frontend POSTs `POST /api/payment/decentralized/verify/:paymentId` with body `{ transactionHash }`. The `paymentId` is a **path parameter**. **Auth:** Bearer JWT required (owner or admin).
|
|
12. Backend `BSCTransactionVerifier.verifyTransaction(txHash)` (`decentralizedPaymentService.ts`):
|
|
- JSON-RPC `eth_getTransactionReceipt` against `bsc-dataseed.binance.org`.
|
|
- Confirms `receipt.status === '0x1'` (success).
|
|
- Computes confirmations = `current eth_blockNumber - receipt.blockNumber`.
|
|
- Optionally decodes the `Transfer` event log to confirm `from`, `to`, and `value` match the expected payment.
|
|
13. On success the backend:
|
|
- Updates the `Payment`: `status = 'completed'`, `escrowState = 'funded'`, `blockchain.transactionHash`, `blockchain.confirmations`, `blockchain.confirmedAt = now`.
|
|
- Triggers the **same funded-escrow cascade**: mark winning offer accepted, reject others, transition request to `payment`, create chat, send notifications, emit socket events.
|
|
14. Returns `{ status: 'confirmed', confirmations, blockNumber }`.
|
|
|
|
> [!warning] ⚠️ Stats undercounting — `'completed'` not counted as successful
|
|
> The admin stats aggregate counts only payments with `status === 'confirmed'` as successful. DePay and SHKeeper payments reach **`'completed'`** as their terminal state (not `'confirmed'`), so the admin success count will be **artificially low**. The aggregate must include both `'confirmed'` and `'completed'` in the success set.
|
|
|
|
### Phase 6 — Frontend reaction
|
|
|
|
15. The checkout UI shows "Payment verified" with the block-explorer link (`https://bscscan.com/tx/{hash}`) and transitions to the awaiting-delivery state.
|
|
|
|
> [!warning] ⚠️ Non-existent status/confirm endpoints — dispute payment card
|
|
> The **dispute payment card** "Verify" button calls `getPaymentStatus()`, which internally hits `GET /payment/:id/status`. This route **does not exist** — there is no `/status` sub-route on any payment document endpoint. The call always returns 404. Similarly, `POST /payment/:id/confirm` **does not exist**; no `/confirm` sub-route is registered. Remove both from any frontend code paths and rely on socket events (`payment-update`, `payment-completed`) or the verify endpoint instead.
|
|
>
|
|
> Additionally, `cancelPayment()` in the web3 context is a **local UI state reset only** — it does **not** make an HTTP call. `DELETE /payment/:id` does not exist; there is no DELETE handler on any payment route.
|
|
|
|
## Sequence diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
autonumber
|
|
actor B as Buyer
|
|
participant W as Wallet (MetaMask/WC)
|
|
participant FE as Frontend (wagmi)
|
|
participant BE as Backend
|
|
participant BC as BSC RPC
|
|
participant DB as MongoDB
|
|
participant IO as Socket.IO
|
|
|
|
B->>FE: Click "Pay with wallet"
|
|
FE->>W: connect()
|
|
W-->>FE: { address, chainId }
|
|
opt chainId != 56
|
|
FE->>W: wallet_switchEthereumChain(0x38)
|
|
end
|
|
FE->>BE: POST /api/payment/decentralized/save
|
|
BE->>DB: Payment.create({provider:"other", direction:"in", receiver:ESCROW})
|
|
BE-->>FE: { paymentId, escrowAddress, amount }
|
|
opt allowance < amount
|
|
FE->>W: approve(escrow, amount)
|
|
W-->>FE: tx confirmed
|
|
end
|
|
FE->>W: transfer(escrow, amount)
|
|
W-->>FE: tx broadcast
|
|
W-->>BC: signed tx
|
|
BC-->>W: tx confirmed
|
|
FE->>BE: POST /api/payment/decentralized/verify/:paymentId { txHash }
|
|
BE->>BC: eth_getTransactionReceipt(txHash)
|
|
BC-->>BE: { status:0x1, blockNumber, logs }
|
|
BE->>BC: eth_blockNumber
|
|
BC-->>BE: currentBlock
|
|
BE->>BE: confirmations = currentBlock - txBlock
|
|
BE->>DB: Payment.status="completed"\nescrowState="funded"\ntx hash + confirmations
|
|
BE->>BE: cascade (mark offer accepted, others rejected,\nrequest→payment, chat, notifications)
|
|
BE->>IO: emit payment-completed events
|
|
BE-->>FE: { status:"confirmed", confirmations }
|
|
FE-->>B: "Payment verified ✓" + BscScan link
|
|
```
|
|
|
|
## API calls
|
|
|
|
| Method | Endpoint | Notes | Source |
|
|
|---|---|---|---|
|
|
| `POST` | `/api/payment/decentralized/save` | Create intent | `decentralizedPaymentRoutes.ts` |
|
|
| `POST` | `/api/payment/decentralized/verify/:paymentId` | `paymentId` is a **path param** | `decentralizedPaymentRoutes.ts` |
|
|
| `POST` | `/api/payment/payments/:id/fetch-tx` | Manual tx rechecker — **NO AUTH** (exploitable without credentials) | `paymentRoutes.ts` |
|
|
| ~~`POST /api/payment/decentralized/create`~~ | | ⚠️ **404 — does not exist.** Use `/save` instead. | — |
|
|
| ~~`GET /payment/:id/status`~~ | | ⚠️ **404 — does not exist.** No `/status` sub-route. | — |
|
|
| ~~`POST /payment/:id/confirm`~~ | | ⚠️ **404 — does not exist.** No `/confirm` sub-route. | — |
|
|
| ~~`DELETE /payment/:id`~~ | | ⚠️ **404 — does not exist.** `cancelPayment()` is UI-only. | — |
|
|
| ~~`POST /payment/depay/intents`~~ | | ⚠️ **NOT IMPLEMENTED** — `createDePayIntent()` target. | — |
|
|
|
|
> [!warning] ⚠️ `/api/payment/payments/:id/fetch-tx` has no authentication
|
|
> The endpoint `POST /api/payment/payments/:id/fetch-tx` (note the `/payments/` infix — the previously documented path `/api/payment/fetch-tx/:paymentId` was wrong on both method and path) accepts requests **without any authentication check**. Any unauthenticated caller can trigger a blockchain re-fetch for any payment ID. This must be gated behind at minimum an admin JWT before production use.
|
|
|
|
### Request Network sub-routes — NOT IMPLEMENTED
|
|
|
|
The following four Request Network payout/release/refund sub-paths are **not registered** in the backend router. All return 404:
|
|
|
|
| Path | Status |
|
|
|---|---|
|
|
| `POST /api/payment/request-network/:id/payout/initiate` | ⚠️ NOT IMPLEMENTED — 404 |
|
|
| `POST /api/payment/request-network/:id/payout/confirm` | ⚠️ NOT IMPLEMENTED — 404 |
|
|
| `POST /api/payment/request-network/:id/release/confirm` | ⚠️ NOT IMPLEMENTED — 404 |
|
|
| `POST /api/payment/request-network/:id/refund/confirm` | ⚠️ NOT IMPLEMENTED — 404 |
|
|
|
|
## Database writes
|
|
|
|
- **`payments`** — same model as the Request Network flow. `provider` distinguishes the source.
|
|
- **`selleroffers`**, **`purchaserequests`**, **`chats`**, **`notifications`** — identical funded-escrow cascade (offer accepted, others rejected, request → `payment`, chat created, notifications fanned out).
|
|
|
|
### Payment status values
|
|
|
|
| `status` | `escrowState` | Meaning |
|
|
|---|---|---|
|
|
| `pending` | — | Intent created, awaiting on-chain transfer |
|
|
| `completed` | `funded` | On-chain transfer verified (terminal success for DePay/wallet-direct) |
|
|
| `failed` | — | Transaction reverted or verification failed |
|
|
|
|
### escrowState values (backend-authoritative)
|
|
|
|
| `escrowState` | Meaning |
|
|
|---|---|
|
|
| `funded` | Escrow received the on-chain transfer |
|
|
| `releasable` | Escrow funds cleared for release to seller |
|
|
| `releasing` | Release to seller in progress (intermediate state) |
|
|
| `released` | Funds sent to seller |
|
|
| `refunding` | Refund to buyer in progress |
|
|
| `refunded` | Funds returned to buyer |
|
|
|
|
> [!note] `'completed'` is not counted as a successful payment in stats
|
|
> `paymentService.getPaymentStats` counts only `status === 'confirmed'` as `successfulPayments`. DePay/wallet-direct payments terminate at `'completed'`, so they are **excluded** from the success count. The aggregate must include `'completed'` alongside `'confirmed'` to avoid undercounting.
|
|
|
|
## Socket events emitted
|
|
|
|
- **`payment-created`** (admin dashboard) on intent creation.
|
|
- **`seller-offer-update`** `'payment-completed'` and `'offer-rejected'` post-verification.
|
|
- **`purchase-request-update`** `'status-changed'`.
|
|
- **`new-notification`** to both parties.
|
|
|
|
## Side effects
|
|
|
|
- **No provider custody** — the escrow wallet is custodial; the platform admin/custody signer controls the keys. Releases from this wallet to sellers should follow [[Payout Flow]] and the Safe/hardware-backed roadmap in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
|
|
- **Verified-wallet field** — the buyer's connected wallet is also saved against `User.profile.walletAddress` (in `WalletConnectionCard`), which is later used to refund this same wallet if the order is disputed.
|
|
|
|
## Error / edge cases
|
|
|
|
- **Wrong network** → frontend forces a chain switch; if user refuses, transaction will fail or hit the wrong chain (escrow is BSC-only today).
|
|
- **Insufficient gas (BNB)** → wallet rejects the tx; nothing to verify.
|
|
- **Transaction reverted** (`receipt.status === '0x0'`) → verifier returns `failed`; backend marks `Payment.status = 'failed'`. Buyer can retry.
|
|
- **Transaction not yet mined** at verification time → verifier returns `pending` with `error: 'Transaction not found or still pending'`. Frontend retries verification on a backoff schedule.
|
|
- **Tx hash already used by another payment** — `blockchain.transactionHash` has a sparse index (`Payment.ts:178`); a duplicate verification attempt finds the existing payment and returns its status.
|
|
- **Wrong amount or wrong recipient** — must be enforced by log decoding (the verifier should reject if the `Transfer` event's `value` is less than expected or `to` ≠ escrow). The current verifier only checks the receipt's `status`; tightening this is recommended.
|
|
- **RPC throttling** — public BSC dataseed is generous but rate-limits exist; consider a dedicated RPC (Ankr, QuickNode) for production.
|
|
- **User closes the browser before verification** — the on-chain transfer still happened. A periodic reconciliation job (`POST /api/payment/payments/:id/fetch-tx`) or admin tool can replay verification from the txHash.
|
|
- **Confirmation depth** — currently 1 confirmation triggers `completed`. For larger amounts consider gating release until ≥ 12 confirmations on BSC.
|
|
|
|
> [!warning] Verify the event log, not just the receipt
|
|
> A receipt status of `0x1` means the transaction did not revert — it does **not** confirm the right amount went to the right address. Decode the ERC-20 `Transfer(address,address,uint256)` event and assert `to == ESCROW_WALLET_ADDRESS` and `value >= expectedAmount`. The current implementation in `decentralizedPaymentService.ts` checks only the receipt status; harden this before accepting large payments.
|
|
|
|
## Linked flows
|
|
|
|
- [[PRD - Request Network In-House Checkout]] — current primary checkout.
|
|
- [[Payment Flow - SHKeeper]] — historical sibling pay-in path retained for migration context.
|
|
- [[Escrow Flow]] — funded state semantics.
|
|
- [[Payout Flow]] — releasing the funded escrow to the seller.
|
|
- [[Dispute Flow]] — refunds back to the buyer's verified wallet.
|
|
|
|
## Source files
|
|
|
|
- Backend: `backend/src/services/payment/decentralizedPaymentService.ts`
|
|
- Backend: `backend/src/services/payment/decentralizedPaymentRoutes.ts`
|
|
- Backend: `backend/src/services/payment/paymentCoordinator.ts`
|
|
- Backend: `backend/src/models/Payment.ts`
|
|
- Frontend: `frontend/src/web3/web3Service.ts`
|
|
- Frontend: `frontend/src/web3/context/wagmi-provider.tsx`
|
|
- Frontend: `frontend/src/web3/context/web3-provider.tsx`
|
|
- Frontend: `frontend/src/sections/account/account-wallet-connection.tsx`
|
|
- Frontend: `frontend/src/sections/request/components/buyer-steps/payment-card.tsx`
|
|
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx`
|
|
- Frontend: `frontend/src/global-config.ts` (`ESCROW_WALLET_ADDRESS`)
|