Files
nick-doc/04 - Flows/Payment Flow - DePay & Web3.md
Siavash Sameni 81625d35d2 docs: AML scope note, human-blocked items, Task #11 pre-flight inventory
- Add AML scope note to Handoff - RN Multichain Probe (sanctions-only vs full KYT)
- Add human-blocked section with 3 precise next steps for owner
- Create Task 11 Pre-flight Inventory: library choice, dev/prod flow, admin UI gaps, backend gaps, risks, acceptance criteria
2026-05-28 20:42:42 +04:00

11 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/create
POST /api/payment/decentralized/verify

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.

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' depending on enum extension), direction: 'in', status: 'pending', blockchain.{network, token, sender, receiver: ESCROW_WALLET_ADDRESS}. Auth: Bearer JWT required.
  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).

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 { transactionHash }. 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 }.

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.

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/create
    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 Source
POST /api/payment/decentralized/create decentralizedPaymentRoutes.ts
POST /api/payment/decentralized/verify decentralizedPaymentRoutes.ts
GET /api/payment/fetch-tx/:paymentId paymentRoutes.ts (manual rechecker)

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 (/api/payment/fetch-tx/:paymentId) 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)