From 31dd475b735937d0cfbda80b9d6fe6df908103e0 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 28 May 2026 16:05:50 +0400 Subject: [PATCH] =?UTF-8?q?docs(prd):=20clarify=20task=20#7=20keying=20?= =?UTF-8?q?=E2=80=94=20cart-with-multi-seller,=20per-Payment=20derivation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User flagged: a buyer's cart can span multiple sellers, so 'per-(buyer, seller)' isn't really 1:1. The right framing is per-Payment: Amanat already creates N Payment records for an N-seller cart (one per sellerOfferId), and each gets its own derived destination + RN intent + buyer-side approve+pay tx pair. PRD now explicitly: - Recommends per-Payment keying (which collapses to per-(buyer, sellerOfferId) via the existing uniq_pending_request_network_by_buyer_session index) - Documents the multi-seller cart UX (N approve+pay pairs in sequence, with clear progress indicator, mid-cart abandonment is fine) - Notes RN's ERC20FeeProxy is single-destination by design (no atomic split in v1; future Amanat splitter contract is out of scope) - Updates open questions to monotonic derivation counter, immediate sweep, single-use addresses (no rotation), and cold-payment recovery - Scope explicitly mentions cart-aware buyer UX as part of task #7 Co-Authored-By: Claude Opus 4.7 --- .taskmaster/tasks/tasks.json | 36 +++++++-------- ... Multichain, Confirmations, AML, Trezor.md | 45 +++++++++++++------ 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 880738a..ae84ab5 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -2,7 +2,7 @@ "master": { "tasks": [ { - "id": 1, + "id": "1", "title": "Stabilize Mermaid diagram rendering across documentation vault", "description": "Correct Mermaid syntax/rendering issues across the documentation vault and validate all Mermaid blocks.", "details": "Source PRD: .taskmaster/docs/prd-mermaid-diagram-rendering-stabilization.md. Scope covered 57 Mermaid blocks and 11 failing blocks. The source PRD records that all targeted files now pass mmdc parse validation and the full vault sweep passes.", @@ -47,7 +47,7 @@ ] }, { - "id": 2, + "id": "2", "title": "Implement platform audit remediation plan", "description": "Address the code-backed security and consistency issues identified in the 2026-05-24 platform audit remediation PRD.", "details": "Source PRD: .taskmaster/docs/prd-platform-audit-remediation-plan-2026-05-24.md. Target backend hardening first, then documentation/runtime alignment. Delivery order suggested by PRD: security/auth, rate limiting, passkeys, Web3 verification, socket hardening, dispute hold controls, docs/API alignment.", @@ -154,7 +154,7 @@ ] }, { - "id": 3, + "id": "3", "title": "Migrate payment architecture toward Request Network and internal funds management", "description": "Plan and implement provider-neutral payment flows, Request Network pay-in support, funds ledger, webhook reconciliation, release/refund orchestration, UI migration, and SHKeeper decommissioning.", "details": "Source PRD: .taskmaster/docs/prd-request-network-migration-and-funds-management.md. The PRD recommends phased migration behind a provider adapter, Secure Payment Pages first, platform-controlled escrow/payee destination, and a first-class internal funds ledger before release/refund enforcement.\n\nPost-completion update: Task 3 now includes a CI-safe focused verification command for the provider-neutral payment migration plus optional Trezor safekeeping. Trezor safekeeping is optional by default via TREZOR_SAFEKEEPING_REQUIRED=false and only gates release/refund confirmation when explicitly enabled. Vault references: 04 - Flows/Trezor Safekeeping Flow.md, 03 - API Reference/Trezor API.md, and 08 - Operations/Payment and Trezor Verification Report.md.", @@ -341,7 +341,7 @@ "updatedAt": "2026-05-24T07:04:01.906Z" }, { - "id": 4, + "id": "4", "title": "Define backend security and refactor strategy from latest audit", "description": "Convert the backend stack security/refactor assessment into concrete architecture decisions, documentation deliverables, and developer handoff criteria.", "details": "Source audit: .taskmaster/docs/audit-backend-stack-security-and-refactor-assessment-2026-05-24.md. This task is advisory/architecture-focused and should run in parallel with immediate hardening. It should produce the decision artifacts needed before any backend-core rewrite or provider migration is started.", @@ -483,7 +483,7 @@ "updatedAt": "2026-05-24T07:23:44.643Z" }, { - "id": 5, + "id": "5", "title": "Deliver Telegram-native app, bot, and wallet experience", "description": "Create a Telegram bot plus Mini App surface so users can complete Amanat buyer, seller, escrow, chat, dispute, payment, release/refund, and support workflows from inside Telegram.", "details": "Source PRD: .taskmaster/docs/prd-telegram-native-app-bot-wallet.md. Keep this as a separate delivery track from security remediation and Request Network migration. Identity, bot navigation, Mini App shell, and notifications can start behind flags; wallet/payment crediting and release/refund actions must use canonical backend authorization, provider adapter, funds ledger, escrow state machine, idempotency, and dispute holds.", @@ -647,7 +647,7 @@ "updatedAt": "2026-05-24T13:46:14.458Z" }, { - "id": 6, + "id": "6", "title": "Request Network in-house checkout (Rabby-supporting)", "description": "Replace the redirect to pay.request.network with an Amanat-rendered checkout page that submits the same on-chain calls as RN's hosted UI, so RN's webhook fires unchanged but buyers stay on amn.gg and Rabby works.", "details": "See PRD: nick-doc/.taskmaster/docs/prd-request-network-in-house-checkout.md (summary at nick-doc/PRD - Request Network In-House Checkout.md). Status: draft, pending review with second developer. Approach: replicate the two on-chain calls (approve + RN_FEE_PROXY.transferFromWithReferenceAndFee) using wagmi v2 with existing injected()/metaMask() connectors (Rabby works via EIP-6963). Hard-known: proxy 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9, selector 0xc219a14d, paymentRef = last8Bytes(keccak256(requestId+salt+dest)), feeAmount=0, feeAddress=0x...dEaD. Backend: extend POST /payment/request-network/intents response with inHouseCheckout object (destination, tokenAddress, decimals, chainId, proxyAddress, paymentReference, feeAmount, feeAddress, amountWei). Frontend: new page /checkout/request-network/:paymentId with state machine reusing manual-payment.tsx layout chrome, hosted-page link kept as escape hatch. Implementation gated on a $0.50 cold probe on dev BSC to confirm RN's webhook fires for an externally-built tx. Out of scope: per-seller multi-chain config (§2), ephemeral wallets (§3), full RN removal (§4), gasless. Open questions in PRD §10.", @@ -671,18 +671,19 @@ "updatedAt": "2026-05-28T07:34:40.368Z" }, { - "id": 7, + "id": "7", "title": "Per-(buyer, sellerOffer) ephemeral RN destination wallets", "description": "Replace the single shared Amanat destination wallet with a per-(buyerId, sellerOfferId) HD-derived address sent to Request Network on intent creation, plus sweep-on-approval and an admin UI.", "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §1. Files: new backend/src/services/payment/wallets/derivedDestinations.ts (getDestinationFor(buyerId, sellerOfferId) → {address, derivationPath, chainId}); Payment schema add metadata.derivedDestination; requestNetworkPayInService.ts override destinationId before POST /v2/secure-payments (we confirmed RN accepts different destinations per intent); new sweep cron + admin manual-trigger endpoint gated on Transaction Safety Provider; admin UI at /dashboard/admin/derived-destinations with address, balance, last sweep tx (BscScan link), ownership status. Open questions to settle first: HD vs disposable EOAs vs smart-forwarder (recommended HD); sweep cadence (recommended immediate); granularity (recommended per-(buyer, seller), not per-payment); re-use vs rotate after sweep. KMS-rooted seed; backend never holds raw private keys; signing via KMS API (Task #11 Trezor flow is the longer-term replacement). Acceptance: two payments from one buyer to two sellers land on two different addresses; RN webhook fires for both; sweep is idempotent; master seed never leaves KMS.", "testStrategy": "", - "status": "pending", + "status": "in-progress", "dependencies": [], "priority": "high", - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-28T11:51:34.115Z" }, { - "id": 8, + "id": "8", "title": "Multichain RN proxy registry + USDC/USDT support", "description": "Probe and persist RN ERC20FeeProxy addresses on BSC/Arb/ETH/Polygon/Base, add USDC + USDT token entries with correct decimals per chain, and surface an admin networks page. Include the USDT-mainnet approve(0) reset quirk in the frontend approve step.", "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §2. Tasks: new backend/scripts/probe-rn-chains.ts that walks each chain in supported-chains.json and verifies the canonical 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9 proxy is the real RN proxy via a known view fn (CREATE2 is deterministic, but verify); promote backend/src/services/payment/requestNetwork/tokens.ts to load from JSON + admin override; add USDT entries on all 5 chains (BSC USDT 18-dec quirk, mainnet/Arb/Polygon/Base USDT 6-dec); buildInHouseCheckoutBlock returns reason='unsupported_chain:' for unknowns; new admin route GET /api/admin/rn/networks + frontend page /dashboard/admin/networks rendering the registry with per-row 'probe again'. Frontend approve flow: if buyer is on Ethereum mainnet AND token is USDT AND current allowance > 0, do approve(spender, 0) first then approve(spender, amount). Acceptance: probe succeeds on at least BSC/Arb/Polygon/ETH/Base; one paid probe on BSC USDT end-to-end; mainnet USDT approve(0) reset works; admin page reflects registry. Dependencies: none — runs in parallel with #9. This is task #8 in the PRD.", @@ -693,7 +694,7 @@ "subtasks": [] }, { - "id": 9, + "id": "9", "title": "Per-chain confirmation thresholds + admin UI", "description": "Make TransactionSafetyProvider's confirmation threshold tunable at runtime per chain via admin UI, with an awaiting-confirmation payments view that shows live confirmations vs threshold.", "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §3. Today TRANSACTION_SAFETY_MIN_CONFIRMATIONS is a global env var, default 12, baked in until redeploy. Move to runtime config: new Setting docs keyed 'confirmation_threshold:' or extend existing model; cache reads in transactionSafetyProvider.ts for 30s; GET/PATCH /api/admin/settings/confirmation-thresholds (auth: admin); new admin page /dashboard/admin/confirmation-thresholds (table: chain, current, recommended default, edit-in-place with confirm dialog, audit log of changes); new admin page /dashboard/admin/payments/awaiting-confirmation (payments where escrowState !== 'funded' AND metadata.transactionSafety.lastCheck.status === 'pending'; for each show tx hash linked to explorer, current confirmations via 12s poll on BSC, threshold, ETA). Acceptance: admin lowers BSC threshold from 12 to 3 on dev, next webhook honors new value within 30s; awaiting-confirmation table updates live; audit log records every change. Non-goals: per-asset, per-seller thresholds. Dependencies: none. This is task #9 in the PRD.", @@ -704,7 +705,7 @@ "subtasks": [] }, { - "id": 10, + "id": "10", "title": "Optional AML screening on incoming payments (seller-paid)", "description": "Turn the existing aml_screening placeholder in TransactionSafetyProvider into a real Chainalysis (or equivalent) Address Screening call that the seller opts into per-offer and pays the per-check cost for.", "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §4. Default provider recommendation: Chainalysis Address Screening (cheapest, simplest). Files: new backend/src/services/payment/safety/amlProvider.ts interface + chainalysisProvider.ts impl behind env TRANSACTION_SAFETY_AML_PROVIDER=chainalysis with API_KEY in KMS; transactionSafetyProvider's evaluateAmlPlaceholder() becomes real, persists raw provider response on Payment.metadata.amlResult; Offer schema add requireAmlCheck + amlBlockOnFailure booleans; offer-edit UI toggle 'Require AML on incoming payments ($X per payment, paid by you)'; admin global config UI for provider selection + API key rotation + per-chain enabled flag; cost accounting: deduct per-check cost from seller's escrow on completion as a separate ledger line item, surfaced on payment-details. Open questions before code: pick provider (Chainalysis vs TRM vs Elliptic — need 1-page comparison of cost/latency/coverage); failure mode (fail-closed only when seller opted in AND amlBlockOnFailure=true, else warn/log); cost batching cadence. Acceptance: seller toggles AML on an offer; incoming payment triggers a real Chainalysis call; sanctions verdict blocks the safety gate; clean verdict passes; seller's settled amount reduced by check cost; admin can rotate API key without redeploy; provider-down + amlBlockOnFailure=true keeps payment pending with provider_unavailable reason. Dependencies: none. This is task #10 in the PRD.", @@ -715,7 +716,7 @@ "subtasks": [] }, { - "id": 11, + "id": "11", "title": "Trezor signing for admin actions (release/refund/sweep)", "description": "Replace the hot-key admin signing flow with a WebUSB-based Trezor flow so the backend never holds a private key. All admin-side txes are built backend, signed via Trezor in the browser, broadcast from the browser.", "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §5. Lib: @trezor/connect-web (WebUSB; Chromium-only — Firefox users need Trezor Bridge native helper). Files: new frontend/src/web3/trezor/trezorConnector.ts wrapping @trezor/connect-web; existing admin actions (release/refund/sweep when #7 lands) get a 'Sign with Trezor' button that flows: POST /api/admin/actions/build-tx → returns unsigned tx bytes → send to Trezor → sign → wagmi sendTransaction broadcasts → POST /api/admin/actions/confirm-tx with hash; admin settings page to register Trezor address(es) (backend rejects signatures from unauthorized devices); audit log on every Trezor-signed action; break-glass hot-key path requires explicit admin toggle, expires after 1h, fires Telegram alarm. Open questions: m-of-n multi-admin signing — default single-signer for v1; Trezor One vs Model T — lib abstracts; fallback when Trezor unavailable — break-glass with alarm. Acceptance: admin registers Trezor address; release flow uses Trezor end-to-end; backend rejects signatures from unregistered devices; audit log captures admin user + Trezor addr + tx hash + before/after escrow state; break-glass works and alarms. Non-goals: mobile Trezor flow, buyer-side Trezor (buyer uses wagmi injected). Dependencies: task #7 (ephemeral wallets) for the sweep step — but task #11 can ship the release/refund flows first. This is task #11 in the PRD.", @@ -728,15 +729,12 @@ ], "metadata": { "version": "1.0.0", - "lastModified": "2026-05-28T07:34:40.369Z", - "taskCount": 6, + "lastModified": "2026-05-28T11:51:34.115Z", + "taskCount": 11, "completedCount": 5, "tags": [ "master" - ], - "created": "2026-05-28T11:47:32.273Z", - "description": "Tasks for master context", - "updated": "2026-05-28T11:48:22.144Z" + ] } } } \ No newline at end of file diff --git a/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md b/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md index 0bcff36..05ff979 100644 --- a/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md +++ b/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md @@ -9,13 +9,23 @@ Five follow-ups to the in-house Request Network checkout. They are sized so a si --- -## 1. Per-(buyer, seller) ephemeral destination wallets — Task #7 +## 1. Per-Payment 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. +For each `Payment` record (which already represents a `(buyerId, sellerOfferId)` pair), 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. + +### Key clarification: cart with multiple sellers + +A buyer's cart can contain items from multiple sellers. **That's already modeled in Amanat as N separate `Payment` records, one per `sellerOfferId` in the cart** — see the `uniq_pending_request_network_by_buyer_session` index in `requestNetworkPayInService.ts`. So: + +- 1 Payment record = 1 derived destination address = 1 RN intent = 1 buyer-side on-chain transaction. +- A 5-item cart spanning 2 sellers produces 2 Payments, 2 derived addresses, 2 RN intents, and the buyer makes 2 approve+pay tx pairs in sequence. +- RN's `ERC20FeeProxy.transferFromWithReferenceAndFee` is single-destination by design — there's no atomic multi-recipient split. A future v2 could route the cart through an Amanat splitter contract that fans out to per-seller derived addresses in one buyer tx; that's out of scope here. + +So the correct keying is **per `Payment._id`**, which collapses to per-`(buyer, sellerOfferId)` because of the existing uniqueness constraint on pending Payments. We do NOT key by `(buyer, seller)` directly — a single seller can have multiple distinct offers a buyer may pay for, and we want each to settle into its own address for audit lineage. ### Hard-known facts (from RN docs we've cold-inspected so far) - The "destination" in RN is the `destinationId` inside the merchant reference: `
@eip155:#:`. RN doesn't bind this to an Amanat-level identity; it's just where the funds end up. @@ -24,20 +34,27 @@ For each `(buyerId, sellerOfferId)` (or `(buyerId, sellerId)` — see open quest ### 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'//`). 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. + - **Deterministic HD wallet** rooted at one Amanat master seed; derive per-Payment path (e.g. `m/44'/60'/0'/`). 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 Payment._id. Sweep then forget. + - **Smart contract per Payment** that auto-forwards to the master wallet on receive. Avoids holding keys at all, but costs gas + an extra hop per payment. + - Recommended starting point: HD wallet, with sweep-on-confirmation. Cheapest, most auditable. Derivation index can come from a monotonic counter (`Setting{key:'rn_derivation_next_idx'}`) so we never re-derive an exhausted address. +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. For BSC USDC gas is cheap enough that immediate is fine; revisit if we add a costly chain. +3. **Multi-seller cart UX.** The buyer signs N approve+pay pairs in sequence (one per Payment). Frontend MUST surface this clearly: + - "You're paying 2 sellers for this order. Please confirm 4 transactions in your wallet." (2 approves + 2 pays) + - Progress indicator: "Paid 1 of 2 sellers — continue?" + - If buyer aborts mid-cart, the sellers who already received funds are settled; the rest stay pending (existing Payment lifecycle handles this). + - Out of scope for v1: an atomic splitter contract that fans out in one buyer tx. +4. **Re-use vs. rotate.** A derived address is single-use by default (one Payment = one address). After sweep, the address is empty and we never reuse it — derivation index marches forward monotonically. This is the cleanest audit story. +5. **Cold-payment recovery.** What if RN reports a payment to the derived address but our backend never created the Payment record (RN paid to a wrong address user inputted manually)? Out of scope for v1: the in-house UI never asks the buyer to type an address. Manual recovery via support. ### 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. +1. New module `backend/src/services/payment/wallets/derivedDestinations.ts` with `getDestinationForPayment(paymentId)` returning `{ address, derivationPath, chainId }`. Idempotent — calling it twice for the same Payment returns the already-allocated address. +2. New `Setting` doc with key `rn_derivation_next_idx` tracking the monotonic derivation counter (atomic `findOneAndUpdate $inc`). +3. Migration on `Payment` schema to add `metadata.derivedDestination` (address + derivation path + chainId snapshot). +4. RN intent creation in `requestNetworkPayInService.ts` calls `getDestinationForPayment(payment._id)` and overrides the destination half of the merchant reference before `createSecurePaymentRequest`. Uses the new `buildMerchantReference` helper (already in `merchantReference.ts`). +5. Sweep job (cron + manual-trigger admin endpoint) under Transaction Safety Provider gate. Sweep target = the master wallet from the env's `REQUEST_NETWORK_MERCHANT_REFERENCE`. +6. Admin UI (table) at `/dashboard/admin/derived-destinations` to view: Payment id, derived address, balance, sweep status, last sweep tx (BscScan link), age, ownership status. +7. Cart-aware buyer UX in the in-house checkout (if a cart spans multiple sellers): clear progress UI, sequential approval flow, recoverable mid-cart abandonment. ### Non-goals - Multi-chain destinations (covered in Task #8).