Files
nick-doc/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md
Siavash Sameni 0060b16912 docs: ship in-house RN checkout, scope 5 follow-up tasks (#7-11)
In-house Request Network checkout went fully end-to-end on dev today.
A real 0.01 USDC payment flowed through wallet connect -> approve ->
ERC20FeeProxy.transferFromWithReferenceAndFee -> RN webhook ->
TransactionSafetyProvider -> Payment.status=completed -> page success
state. Tx 0x494c77a29161b5100d8e0b1ac675f1822955d0bb3633ecdbfafb886f84f2f320.

Docs:
- New PRD: Wallet, Multichain, Confirmations, AML, Trezor
  (5 follow-ups, each sized for an independent contributor)
- Updated PRD: Request Network In-House Checkout (phases 0..3 done,
  phase 4 partial, phases 5-6 not started)
- Updated handoff: deployed versions, what is working end-to-end,
  follow-up tasks index

Taskmaster: 5 new top-level tasks (#7..#11) covering ephemeral
destination wallets, multichain proxy registry + USDC/USDT, runtime
confirmation thresholds, optional seller-paid AML screening, and
Trezor signing for admin actions. Tasks are scoped fine-grained so
each is independent enough for kimi to pick up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:50:24 +04:00

226 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRD: Wallet, Multichain, Confirmations, AML, Trezor
> Status: **Draft — 2026-05-28**
> Author: nick + claude (after in-house RN checkout shipped on dev 2.6.38/2.6.41)
> Owner: backend (payments) + frontend (admin UI + checkout)
> Related: `PRD - Request Network In-House Checkout.md`, `01 - Architecture/Request Network Integration Constraints.md`
Five follow-ups to the in-house Request Network checkout. They are sized so a single contributor can pick up any one of them in isolation. Each is its own Taskmaster top-level task — see `#7…#11`.
---
## 1. Per-(buyer, seller) ephemeral destination wallets — Task #7
### Problem
Today the in-house checkout sends *all* RN-routed payments to one Amanat-controlled wallet (env: `REQUEST_NETWORK_MERCHANT_REFERENCE`). That wallet is shared across every buyer, every seller, every offer. It's both an audit nightmare (no buyer↔settlement linkage at the wallet level) and a single point of compromise.
### Goal
For each `(buyerId, sellerOfferId)` (or `(buyerId, sellerId)` — see open questions), generate a fresh on-chain destination address, persist it on the `Payment` record, and tell Request Network to expect funds on that address. RN's webhook flows unchanged.
### Hard-known facts (from RN docs we've cold-inspected so far)
- The "destination" in RN is the `destinationId` inside the merchant reference: `<address>@eip155:<chainId>#<contractId>:<tokenAddress>`. RN doesn't bind this to an Amanat-level identity; it's just where the funds end up.
- Each `POST /v2/secure-payments` request *can* pass a different `destinationId`. RN doesn't reject divergent destinations across requests from the same client.
- `paymentReference` is derived per request (`last8Bytes(keccak256(requestId+salt+destination))`), so different destinations naturally produce different on-chain refs. The webhook listener keys on the ref + tx hash.
### Open questions to settle before code
1. **Key custody model.** Options:
- **Deterministic HD wallet** rooted at one Amanat master seed; derive per-`(buyer, seller)` path (e.g. `m/44'/60'/0'/<buyerIdx>/<sellerIdx>`). Keys live in the backend, single seed in KMS/HSM. Sweep is one tx per derived addr.
- **One-shot disposable EOAs**, encrypted and stored in Mongo (or KMS), keyed by `(buyer, seller)`. Sweep then forget.
- **Smart contract per offer** that auto-forwards to the master wallet on receive. Avoids holding keys at all, but costs gas + an extra hop.
- Recommended starting point: HD wallet, with sweep-on-confirmation. Cheapest, most auditable.
2. **Sweep strategy.** Sweep immediately on webhook confirmation, or batch sweep cron'd nightly? Trade gas vs. exposure window. Default: sweep immediately under Transaction Safety Provider approval.
3. **Granularity.** Per `(buyer, seller)`, per `(buyer, seller, offer)`, or per single payment? Per-offer gives clean audit lineage; per-payment is overkill (extra derivations); per-`(buyer, seller)` is reusable across multi-step deals.
4. **Re-use vs. expire.** If a derived address has funds in it after sweep, do we still re-use for the same pair's next payment, or rotate? Re-use = simpler, slight privacy hit.
### Scope
1. New module `backend/src/services/payment/wallets/derivedDestinations.ts` with `getDestinationFor(buyerId, sellerOfferId)` returning `{ address, derivationPath, chainId }`.
2. Migration on `Payment` schema to add `metadata.derivedDestination` (address + derivation path snapshot).
3. RN intent creation calls `getDestinationFor(...)` and overrides the destination half of `REQUEST_NETWORK_MERCHANT_REFERENCE`.
4. Sweep job (cron + manual-trigger admin endpoint) under Transaction Safety Provider gate.
5. Admin UI (table) to view derived destinations, their balances, sweep status, and last sweep tx.
### Non-goals
- Multi-chain destinations (covered in Task #8).
- Buyer-side ephemeral keys (covered in `Request Network Integration Constraints.md` §3, separate PRD).
- Hardware-wallet-signed sweeps (covered in Task #11).
### Acceptance criteria
1. Two payments from the same buyer to two different sellers land on two different addresses on-chain.
2. RN's webhook fires correctly for both, regardless of the destination divergence.
3. Sweep runs idempotently — re-running it on an already-swept address is a no-op.
4. Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status.
5. Master seed never leaves the KMS/secret store. Backend reads derivation paths only; signs sweep txes via KMS API.
---
## 2. Multi-chain RN proxy registry + USDC/USDT support — Task #8
### Problem
`backend/src/services/payment/requestNetwork/proxyAddresses.ts` and `tokens.ts` currently hardcode BSC USDC plus a handful of token entries. The in-house checkout will fall through to "in-house checkout not available" the moment a buyer wants to pay on Arbitrum/Polygon/Ethereum, or wants USDT instead of USDC on the same chain.
### Goal
Verified-from-chain registry of:
- RN's `ERC20FeeProxy` address per supported chain.
- USDC + USDT contract address + decimals per chain.
Plus an admin "supported networks" UI that:
- Lists each chain + token combo with its current status (verified-on-chain / probe failed / disabled).
- Shows which chains a given seller has whitelisted (depends on per-seller `acceptedChains` config; out of scope here — see `Request Network Integration Constraints.md` §2).
### Hard-known facts
- RN published their canonical `ERC20FeeProxy` for BSC + Arbitrum as `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` (CREATE2 deterministic). Same address on Ethereum mainnet, Polygon, Base **per the deterministic deployment claim** — needs verifying.
- BSC USDC has 18 decimals (Binance-Peg). Mainnet/Arb/Polygon USDC have 6.
- USDT on Ethereum requires `approve(0)` before a non-zero re-approve. Other chains' USDT don't.
### Scope
1. Probe script `backend/scripts/probe-rn-chains.ts` that walks every chain in `supported-chains.json`, calls a known view fn on the candidate proxy address, and confirms it's a real RN proxy (not just bytes). Emits a report.
2. Promote `tokens.ts` to load from a JSON file + override layer for admin-managed entries. Keep the canonical defaults committed.
3. Backend route `GET /api/admin/rn/networks` returning the registry plus probe status.
4. Frontend admin page `/dashboard/admin/networks` rendering the table.
5. `buildInHouseCheckoutBlock` consults the chain registry; returns `reason='unsupported_chain:<chainId>'` cleanly.
6. Add the USDT-mainnet `approve(0)` quirk to the frontend approve step. When detected, the approve flow does `approve(spender, 0)``approve(spender, amount)`.
### Non-goals
- Letting a seller pick which chain a given buyer can use (separate PRD, §2 in constraints doc).
- Cross-chain bridging.
### Acceptance criteria
1. Probe script run on dev confirms RN proxy address on at least BSC, Arbitrum, Polygon, Ethereum, Base. Differences are documented.
2. Token registry has entries for USDC + USDT on those 5 chains, with correct decimals.
3. In-house checkout supports USDT on BSC end-to-end (a paid probe).
4. USDT-mainnet `approve(0)` reset is handled in the UI when needed.
5. Admin networks page renders the registry with a per-row "probe again" button.
---
## 3. Confirmation-counting + admin threshold UI — Task #9
### Problem
The Transaction Safety Provider already reads `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` from env (default 12). That number is global, baked into env, only changeable by re-deploy. We want it tunable per-chain at runtime by an admin, with a clear UI showing each in-flight payment's current confirmation depth against the threshold.
### Goal
1. Persist per-chain confirmation thresholds in a `ConfigKV` collection (or extend an existing settings model) so admin can adjust without redeploy.
2. The `TransactionSafetyProvider`'s confirmation check reads the runtime threshold first, falls back to env.
3. Frontend admin UI to view + edit per-chain thresholds, and a "payments awaiting confirmation" table that shows `confirmations / threshold` updated live.
### Scope
1. New collection / extension to existing `Setting` model: `{ key: 'confirmation_threshold:<chainId>', value: <int>, updatedBy, updatedAt }`.
2. Public-to-admin endpoint `GET /api/admin/settings/confirmation-thresholds` and `PATCH /api/admin/settings/confirmation-thresholds/:chainId`.
3. Wire `transactionSafetyProvider` to read from this store; cache for 30s to avoid Mongo hammering.
4. Frontend admin page `/dashboard/admin/confirmation-thresholds`:
- Table of chains, current threshold, recommended default (e.g. 12 for BSC).
- Edit-in-place with a confirm dialog.
- Audit-log style "changed by X on Y at Z".
5. Frontend admin page `/dashboard/admin/payments/awaiting-confirmation`:
- Lists payments with `escrowState !== 'funded'` and `metadata.transactionSafety.lastCheck.status === 'pending'`.
- For each: tx hash (linked to explorer), current confirmations (poll-driven), threshold, ETA.
### Non-goals
- Per-asset thresholds (only per-chain).
- Per-seller overrides.
### Acceptance criteria
1. Admin can lower BSC's threshold from 12 to 3 on the live dev stack without a redeploy and a new webhook fires the safety gate with the new value within 30s.
2. Awaiting-confirmation table updates live as new blocks arrive (poll every 12s on BSC).
3. Audit log records every threshold change with admin user, before/after, timestamp.
---
## 4. Optional AML screening (seller-paid) — Task #10
### Problem
The Transaction Safety Provider has an `evaluateAmlPlaceholder()` stub that currently returns `status: skipped`. We want a real AML pass that the seller can *opt into* per-offer, with the seller covering the per-check API cost.
### Goal
1. Pick a provider (Chainalysis Address Screening, TRM Labs, Elliptic — open question). Default recommendation: Chainalysis Address Screening (cheapest, simplest API).
2. Per-offer setting: `requireAmlCheck: true|false` plus `amlCheckPricePaidBy: 'seller'`.
3. When AML required, the in-house checkout webhook hits the provider with the buyer's source address. Result feeds into Transaction Safety Provider as a real `aml_screening` check.
4. Seller's account is debited for the per-check cost from their Amanat balance (or the payment is partially withheld for the cost).
### Hard-known facts
- AML providers charge per-check (USD 0.100.50 typical). Some charge a flat monthly minimum.
- Chainalysis returns categories like `sanctions`, `darknet_marketplace`, `mixer`. Provider doesn't return PII.
- Most providers have rate limits (~50 rps).
### Open questions
1. **Provider choice.** Need a 1-page comparison: per-check cost, response latency, supported chains, sanctions-coverage scope.
2. **Failure mode.** If the AML API is down, do we pass-through (let the payment complete) or fail-closed (block)? Recommended: fail-closed only when seller explicitly opted in *and* enabled "block-on-provider-failure". Otherwise warn + log.
3. **Cost accounting.** Per-check cost is small; do we round up to nearest cent, batch by day, or pass-through exact?
### Scope
1. Add `requireAmlCheck` + `amlBlockOnFailure` fields to the `Offer` schema (or `SellerOffer`).
2. New `backend/src/services/payment/safety/amlProvider.ts` interface + `chainalysisProvider.ts` impl. Behind env flag `TRANSACTION_SAFETY_AML_PROVIDER=chainalysis`.
3. Transaction Safety Provider's `aml_screening` check now real, with `metadata.amlResult` persisted on the Payment record for audit.
4. Cost accounting: deduct per-check cost from seller's escrow on payment completion; surface this as a line item on the payment-details view.
5. Frontend offer-edit UI: a toggle "Require AML on incoming payments (cost: $X per payment, paid by you)".
6. Frontend admin UI for global AML provider configuration (provider, API key, per-chain enabled/disabled).
### Non-goals
- Buyer-side AML (we screen the buyer's *source* address, not the seller's identity).
- Custom AML rules / scoring beyond the provider's verdict.
### Acceptance criteria
1. A seller can opt into AML on a specific offer. Toggle persists.
2. An incoming payment to that offer triggers a real Chainalysis API call; the result is stored on the Payment record.
3. Verdict `sanctions` blocks the escrow gate; verdict `clean` lets it through.
4. The seller's settled amount is reduced by the AML check cost; a corresponding ledger entry is created.
5. Admin can rotate the Chainalysis API key without redeploy.
6. If the provider is down and `amlBlockOnFailure=true`, the payment stays in pending; a `provider_unavailable` reason surfaces in the admin dashboard.
---
## 5. Trezor support for admin signing — Task #11
### Problem
Today, admin actions that require signing (escrow release, refund, sweep of derived destinations once Task #7 ships) run from a hot-key in the backend env (`ADMIN_PRIVATE_KEY` or similar). That key is a single-point-of-compromise for all custodial funds.
### Goal
Replace the hot-key flow with a Trezor-mediated browser flow:
1. Admin connects a Trezor via WebUSB in the admin dashboard.
2. Admin approves an action in the UI; the unsigned transaction is built backend-side, sent to the browser, signed by the Trezor, broadcast from the browser.
3. The backend never has the private key. The Trezor seed never touches a network.
### Hard-known facts
- `@trezor/connect-web` is the maintained library for Trezor in browser. EIP-1193-compatible adapter exists.
- Trezor supports EVM signing for any chain; chain ID is part of the tx.
- WebUSB is Chromium-only. Firefox users need the Trezor Bridge native helper.
### Open questions
1. **Multi-admin.** If two admins both have a Trezor configured, do they both need to sign (m-of-n), or any one of them? Default: any one of them; m-of-n is out of scope here.
2. **Trezor model.** Trezor One vs Model T have different signing UX. We target both; the lib abstracts it.
3. **Fallback.** What if Trezor is unavailable when an urgent release is needed? Default: a "break-glass" hot-key path that admin can flip on for a 1-hour window, alarms blast Telegram.
### Scope
1. New module `frontend/src/web3/trezor/trezorConnector.ts` wrapping `@trezor/connect-web`.
2. Admin actions (release/refund/sweep) get a "Sign with Trezor" button that:
- Hits backend `POST /api/admin/actions/build-tx` returning unsigned tx bytes.
- Sends to Trezor for signing.
- Submits signed tx via wagmi `sendTransaction`.
- Calls `POST /api/admin/actions/confirm-tx` with the tx hash.
3. Backend supports both `confirmReleaseTx` flow (existing) and the new build-tx pattern.
4. Admin settings page to "register Trezor": stores the Trezor's address(es) so backend can reject signatures from unauthorized devices.
5. Audit log on every Trezor-signed action.
### Non-goals
- Multi-sig contracts (Safe etc.) — separate decision.
- Buyer-side Trezor (buyer already uses their own wallet via wagmi `injected()`).
- Mobile Trezor flow (desktop only for v1).
### Acceptance criteria
1. Admin can register a Trezor address; subsequent admin actions show "sign with Trezor" CTA.
2. End-to-end release of escrow: build → Trezor approves → tx broadcast → backend confirms.
3. If Trezor is unregistered or admin tries to sign with a different device, the backend rejects the confirm step.
4. Audit log entries include admin user, Trezor address, tx hash, action, before/after escrow state.
5. Break-glass hot-key path requires an explicit admin toggle, expires after 1h, fires a Telegram alarm.
---
## Shared dependencies / order-of-operations
- Task #8 (multichain) and Task #9 (confirmations) are independent and can land in parallel.
- Task #7 (ephemeral wallets) depends on Task #11 (Trezor) only for the sweep step — the address-generation half can ship first, sweep can land later with hot-key, then migrate to Trezor when #11 is done.
- Task #10 (AML) depends on nothing from the other four. It plugs into the existing Transaction Safety Provider.
- Task #11 (Trezor) is self-contained.
Tasks were sized for one experienced contributor to take any single one end-to-end without needing the others to land first. The integration glue (UI placement, navigation, telemetry) is left for the maintainer.