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

16 KiB
Raw Blame History

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.