Files
nick-doc/04 - Flows/Payment Flow - DePay & Web3.md
Siavash Sameni a1f056e6a5 docs: align flow docs with code reality + create 35 implementation issue files
Flow docs updated (11 files):
- Delivery Confirmation: reversed actor roles (buyer generates, seller verifies),
  fixed endpoint paths (/delivery-code/generate, /delivery-code/verify)
- Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server
  attestation is implemented; refresh tokens are persisted
- Dispute: corrected resolve schema (action enum), removed non-existent statuses,
  documented security gaps (no role guards on status/resolve/assign), route shadowing,
  all socket events are TODO stubs
- Seller Offer: corrected all endpoint paths, removed 'active' status, documented
  withdraw dead code, missing seller history page, select-offer notification gap
- Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup,
  added unread-count-update socket event
- Authentication: corrected rate limiter (counts all attempts), axios 403 not handled,
  deleteAccount wrong endpoint bug, changePassword no UI
- Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on
  reset-with-code vs token reset
- Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk,
  PaymentProvider type gap, getProviderIntentEndpoint routing bug
- Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths
- Purchase Request: added pending_payment/active statuses, fixed sellers/attachments
  endpoints, corrected socket events, PUT→PATCH bug
- Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap

Issues created (35 files in Issues/):
- 9 security issues (critical) including: dispute privilege escalation ×4,
  unauthenticated payment/scanner endpoints ×2, SIM_ production bypass,
  confirm-delivery ownership gap
- 26 additional major/critical bugs covering broken endpoints, missing features,
  data integrity gaps, and frontend-backend mismatches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:47:49 +04:00

16 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Payment Flow - DePay & Web3
flow
payment
web3
wagmi
walletconnect
bsc
Payment
PurchaseRequest
POST /api/payment/decentralized/save
POST /api/payment/decentralized/verify/:paymentId

[!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.
  • Frontendfrontend/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.
  • BackenddecentralizedPaymentService.ts (intent), BSCTransactionVerifier (on-chain verification), decentralizedPaymentRoutes.ts.
  • Blockchain (BSC) — verified via https://bsc-dataseed.binance.org/ JSON-RPC.
  • MongoDBpayments collection, with provider distinguishing the legacy wallet-direct source from Request Network.
  • Socket.IOpayment-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

  1. 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.

  2. 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)

  1. The frontend checks the user's current allowance via useReadContract / allowance(owner, spender) on the USDT contract.
  2. 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

  1. Frontend calls transfer(escrowAddress, amount) on the USDT contract via wagmi's useWriteContract. The wallet popup asks the user to confirm gas.
  2. The buyer signs; the transaction is broadcast.
  3. Frontend listens via wagmi's useWaitForTransactionReceipt({ hash }). On success (1 confirmation), it captures the transactionHash.

Phase 5 — Backend verification

  1. Frontend POSTs POST /api/payment/decentralized/verify/:paymentId with body { transactionHash }. The paymentId is a path parameter. Auth: Bearer JWT required (owner or admin).
  2. 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.
  3. 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.
  4. 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

  1. 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

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 IMPLEMENTEDcreateDePayIntent() 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).

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 paymentblockchain.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

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)