Initial commit: nick docs
This commit is contained in:
172
04 - Flows/Payment Flow - DePay & Web3.md
Normal file
172
04 - Flows/Payment Flow - DePay & Web3.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Payment Flow - DePay & Web3
|
||||
tags: [flow, payment, web3, wagmi, walletconnect, bsc]
|
||||
related_models: ["[[Payment]]", "[[PurchaseRequest]]"]
|
||||
related_apis: ["POST /api/payment/decentralized/create", "POST /api/payment/decentralized/verify"]
|
||||
---
|
||||
|
||||
# Payment Flow — DePay & Web3 (Wallet-Direct)
|
||||
|
||||
Alternative pay-in path: instead of routing through [[Payment Flow - SHKeeper]], 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 (same model as SHKeeper, different `provider` value).
|
||||
- **Socket.IO** — `payment-created`, plus the cascade events from [[Payment Flow - SHKeeper]] 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
|
||||
|
||||
4. Frontend POSTs `POST /api/payment/decentralized/create` 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}`.
|
||||
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`).
|
||||
|
||||
### 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` with `{ paymentId, transactionHash }`.
|
||||
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 cascade** as the SHKeeper webhook: mark winning offer accepted, reject others, transition request to `payment`, create chat, send notifications, emit socket events.
|
||||
14. Returns `{ status: 'confirmed', confirmations, blockNumber }`.
|
||||
|
||||
### 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.
|
||||
|
||||
## 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/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 SHKeeper flow. `provider` distinguishes the source.
|
||||
- **`selleroffers`**, **`purchaserequests`**, **`chats`**, **`notifications`** — identical cascade to [[Payment Flow - SHKeeper]] (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 SHKeeper involvement** — the escrow wallet is custodial; the platform admin holds the keys. Payouts from this wallet to sellers happen via [[Payout Flow]] (SHKeeper payouts API) or manual admin signing using `admin-wallet-payout.tsx` UI.
|
||||
- **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 (`/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
|
||||
|
||||
- [[Payment Flow - SHKeeper]] — sibling pay-in path; same downstream cascade.
|
||||
- [[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`)
|
||||
Reference in New Issue
Block a user