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
This commit is contained in:
Siavash Sameni
2026-05-28 20:42:42 +04:00
parent ddc0434819
commit 81625d35d2
18 changed files with 398 additions and 113 deletions

View File

@@ -301,7 +301,10 @@ Jest test suites in `backend/__tests__/`:
| `models.test.ts` | Schema validation, virtuals, hooks |
| `payment-services.test.ts` | Payment orchestration logic |
| `complete-backend.test.ts` | Cross-service integration |
| Request Network / payment tests | Request Network adapter, webhook signature, ledger, release/refund orchestration |
| `request-network-webhook.test.ts` | Request Network webhook signature and processing |
| `request-network-adapter.test.ts` | Request Network payment adapter |
| `payment-ledger.service.test.ts` | Ledger append/reconciliation behavior |
| `payment-release-refund-orchestration.test.ts` | Release/refund instruction orchestration |
Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`, `npm run test:payment`, etc. when iterating on a slice.

View File

@@ -211,7 +211,7 @@ const config = createConfig({
});
```
Wallet UI: connect / disconnect / show address / show balance via `use-web3-wagmi`, `use-web3-context`. The DePay widget (`@depay/widgets`) is loaded for the assisted-pay flow.
Wallet UI: connect / disconnect / show address / show balance via `use-web3-wagmi`, `use-web3-context`. The current checkout target is the Request Network in-house flow; the DePay widget package remains legacy/frontier context and should not be treated as the primary path.
---

View File

@@ -205,4 +205,4 @@ See [[PRD - Request Network In-House Checkout]] and [[Request Network Integratio
- [[Real-time Layer]] — Socket.IO setup, rooms, events
- [[Security Architecture]] — auth, hashing, rate-limit, webhook HMAC
- [[Tech Stack]] — exact versions & purpose of every dependency
- [[Payment Flow - SHKeeper]] — end-to-end crypto pay-in flow
- [[Escrow Flow]] — current Request Network pay-in, ledger, and custody release flow

View File

@@ -8,11 +8,10 @@ aliases: [Complaint, IDispute]
Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures the reason, priority, category, an array of evidence uploads, a chronological `timeline` of actions, an optional resolution, and SLA deadlines. An admin (`adminId`) is assigned during triage and resolves the dispute with a structured action (`refund`, `replacement`, `compensation`, `warning_seller`, `ban_seller`, or `no_action`).
> [!warning] Missing model
> **`backend/src/models/Dispute.ts` does not exist** as of the 2026-05-24 audit. The `Dispute` model, service layer, and API routes are **documented but not yet implemented** in the backend. The schema below reflects the *intended* design only.
> [!note] Implementation status
> `backend/src/models/Dispute.ts`, `backend/src/services/dispute/DisputeService.ts`, `backend/src/routes/disputeRoutes.ts`, and release-hold helper routes now exist. The remaining gap is canonical state alignment between the full dispute document and the lighter `PurchaseRequest`/`Payment` hold flags used by release gating.
>
> Source (intended): `backend/src/models/Dispute.ts:69` — schema definition
> `backend/src/models/Dispute.ts:238` — model export
> Source: `backend/src/models/Dispute.ts` — schema definition and model export.
## Schema
@@ -59,7 +58,7 @@ None defined.
## Indexes
Defined at `backend/src/models/Dispute.ts:212-223` *(intended)*:
Defined at `backend/src/models/Dispute.ts`:
- `{ purchaseRequestId: 1 }`
- `{ buyerId: 1 }`
@@ -76,7 +75,7 @@ Defined at `backend/src/models/Dispute.ts:212-223` *(intended)*:
| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/Dispute.ts:226` *(intended)*) | On new documents pushes a `dispute_created` entry into `timeline` attributed to `buyerId`. |
| `pre('save')` (`backend/src/models/Dispute.ts`) | On new documents pushes a `dispute_created` entry into `timeline` attributed to `buyerId`. |
## Instance Methods

View File

@@ -5,12 +5,12 @@ tags: [api, dispute, reference]
# Dispute API
> [!warning] Not implemented
> The Dispute module is **documented but not yet implemented** in the backend. There is no `backend/src/services/dispute/` directory, no `backend/src/routes/disputeRoutes.ts`, and no `/api/disputes` mount in `app.ts`. The API specification below reflects the *intended* design only.
> [!note] Current implementation
> The Dispute module now has a Mongoose model, controller routes, dashboard routes, and release-hold helper routes mounted under `/api/disputes`. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`.
Endpoints are planned to live under `/api/disputes/*`. The router would be `backend/src/routes/disputeRoutes.ts` and delegate to `DisputeController` (`backend/src/controllers/disputeController.ts`). The router would apply `authenticateToken` globally — every endpoint requires `Bearer JWT`.
Endpoints live under `/api/disputes/*`. `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. The routers apply `authenticateToken` globally — every endpoint requires `Bearer JWT`.
Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[Payment]] and is the input to the mediation workflow that ends in either a `resolved_buyer` or `resolved_seller` decision and triggers an escrow release or refund via the [[Payment API]].
Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[Payment]] context and is the input to the mediation workflow that can lead to refund, replacement, compensation, warning/ban, or no-action. Release/refund execution should go through the ledger-gated [[Payment API]] and [[Payout Flow]].
## Create
@@ -34,6 +34,18 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
- Notifies the counter-party via `POST /api/notifications` (`new-notification` socket event).
- Pauses any in-flight payout (sets a hold flag on the related [[Payment]]).
### POST /api/disputes/:purchaseRequestId/raise
**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed.
**Auth required:** Bearer JWT (buyer who owns the request or admin)
**Request body:** `{ reason?: string }`
**Response 200:** `{ success, message, data }`
### GET /api/disputes/:purchaseRequestId/status
**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked.
**Auth required:** Bearer JWT (buyer, preferred seller, or admin)
## Read
### GET /api/disputes
@@ -88,11 +100,17 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
```
**Response 200:** `{ success, data: { dispute, paymentAction } }`
**Side effects:**
- `decision === "buyer"` → triggers `POST /api/payment/shkeeper/:id/refund` flow.
- `decision === "seller"` → triggers `POST /api/payment/shkeeper/:id/release` flow.
- `decision === "split"` → admin executes both partial release and partial refund manually.
- `action === "refund"` → create/approve the corresponding refund instruction through the ledger-gated payment release/refund flow.
- `action === "no_action"` or seller-favorable outcome → clear hold only after release checks pass.
- split outcomes require explicit partial release/refund instructions.
- Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`.
### POST /api/disputes/:purchaseRequestId/resolve
**Description:** Lightweight release-hold endpoint that clears the disputed hold flags on a purchase request and related payments.
**Auth required:** Bearer JWT (admin)
**Response 200:** `{ success, message, data }`
## Evidence and messages
### POST /api/disputes/:id/evidence

View File

@@ -35,7 +35,7 @@ Uncaught errors are formatted by [`shared/middleware/errorHandler.ts`](../../bac
}
```
Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy paths, `/api/payment/decentralized/*`, parts of `/api/payment/shkeeper/*`) return ad-hoc shapes such as `{ "error": "..." }` or `{ "success": false, "message": "..." }`. Treat any non-`2xx` response as an error and read whichever of `error` / `message` is present.
Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy paths, and `/api/payment/decentralized/*`) return ad-hoc shapes such as `{ "error": "..." }` or `{ "success": false, "message": "..." }`. Treat any non-`2xx` response as an error and read whichever of `error` / `message` is present.
## HTTP status mapping
@@ -43,7 +43,7 @@ Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy path
| --- | --- | --- |
| `200 OK` | Successful read or mutation | Most `GET`s, idempotent `PUT`s/`PATCH`s |
| `201 Created` | Resource created | `POST /api/marketplace/purchase-requests`, `POST /api/auth/register` (when user created), `POST /api/marketplace/reviews` |
| `202 Accepted` | Async accepted (provider webhooks) | SHKeeper webhook acknowledgement |
| `202 Accepted` | Async accepted (provider webhooks) | Request Network webhook accepted while safety checks are pending |
| `204 No Content` | Mutations with no body to return | Rare — most endpoints return the updated object |
| `400 Bad Request` | Validation failure, malformed input | `express-validator` errors, bad MongoIds, missing fields |
| `401 Unauthorized` | Missing or invalid JWT | `Access token required`, `Invalid or expired token` |
@@ -53,7 +53,7 @@ Legacy routes (chiefly `/api/users` legacy paths, `/api/marketplace` legacy path
| `423 Locked` | Account temporarily locked | After repeated failed logins (Redis-tracked) |
| `429 Too Many Requests` | Rate limit hit | Currently issued only by per-feature Redis limits (auth / AI); global limiter is disabled |
| `500 Internal Server Error` | Unhandled exception | Caught by `errorHandler`; included stack trace in dev |
| `502 Bad Gateway` | Upstream provider failure | OpenAI / SHKeeper unreachable |
| `502 Bad Gateway` | Upstream provider failure | OpenAI / Request Network unreachable |
## Application error codes
@@ -89,11 +89,10 @@ Handled in `errorHandler`:
| Provider | Endpoint | Status on success | Status on signature mismatch |
| --- | --- | --- | --- |
| SHKeeper pay-in | `POST /api/payment/shkeeper/webhook` | 200 `{ success: true }` | 401 `{ success: false }` (then ignored) |
| SHKeeper payout | `POST /api/payment/shkeeper/payout/webhook` | 200 / 400 with `{ success, message, data }` | 400 |
| Request Network pay-in | `POST /api/payment/request-network/webhook` | 200 `{ success: true }` or 202 while safety checks are pending | 401 `{ success: false }` |
| Generic payment callback | `POST /api/payment/callback` | 200 `{ success: true, message }` | 400 |
If a webhook is acknowledged with non-2xx, the provider re-delivers (SHKeeper retries every 60 seconds).
If a webhook is acknowledged with non-2xx, the provider may re-deliver. Persisting delivery evidence and replay support is a launch-hardening item in [[Request Network Integration Constraints]].
## Client guidance

View File

@@ -1,19 +1,21 @@
---
title: Payment API
tags: [api, payment, reference, shkeeper]
tags: [api, payment, reference, request-network, escrow]
---
# Payment API
The payment surface is split across four routers, all mounted under `/api/payment/*`:
The payment surface is split across provider-neutral payment routers, Request Network checkout/webhook routes, derived-destination custody routes, and admin safety routes:
| Path prefix | File | Purpose |
| --- | --- | --- |
| `/api/payment/*` | [`paymentControllerRoutes.ts`](../../backend/src/services/payment/paymentControllerRoutes.ts) | New controller pattern (CRUD + configuration) |
| `/api/payment/*` | [`paymentRoutes.ts`](../../backend/src/services/payment/paymentRoutes.ts) | Additional legacy endpoints (tx fetch, exports) |
| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | DePay / Web3 confirmations |
| `/api/payment/shkeeper/*` | [`shkeeper/shkeeperRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperRoutes.ts) | SHKeeper pay-in, webhook, release/refund |
| `/api/payment/shkeeper/payout*` | [`shkeeper/shkeeperPayoutRoutes.ts`](../../backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts) | SHKeeper payouts to sellers |
| `/api/payment/request-network/*` | [`requestNetwork/requestNetworkRoutes.ts`](../../backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts) | Request Network intent creation, in-house checkout payloads, webhook processing |
| `/api/payment/derived-destinations/*` | [`wallets/derivedDestinationRoutes.ts`](../../backend/src/services/payment/wallets/derivedDestinationRoutes.ts) | Derived destination inspection, balance checks, and sweeping |
| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | Legacy wallet-direct confirmations |
| `/api/admin/rn/networks/*` | [`requestNetwork/networkRegistryRoutes.ts`](../../backend/src/services/payment/requestNetwork/networkRegistryRoutes.ts) | Request Network chain/token registry |
| `/api/admin/payments/awaiting-confirmation/*` | `awaitingConfirmationRoutes.ts` | Admin queue for payments waiting on confirmation/safety checks |
Core model: [[Payment]]. Coordination logic to avoid race conditions when multiple sources update the same payment is in `paymentCoordinator.ts`.
@@ -21,7 +23,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
### POST /api/payment/configuration
**Description:** Returns the payment provider configuration the SHKeeper widget needs (accepted blockchains, escrow receiver address, redirect URLs, webhook URL).
**Description:** Returns the active payment provider configuration, including Request Network settings, supported chain/token data, receiver/derived-destination context, and redirect/webhook URLs where applicable.
**Auth required:** No
**Request body:** `{ amount?, currency?, purchaseRequestId? }` (used to scope returned config)
**Response 200:**
@@ -29,7 +31,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
{
"accept": [{ "blockchain": "bsc", "token": "0x55d3...", "receiver": "0xa30..." }],
"redirect": { "success": "...", "cancel": "..." },
"webhook": "https://.../api/payment/shkeeper/webhook"
"webhook": "https://.../api/payment/request-network/webhook"
}
```
@@ -37,18 +39,18 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
**Description:** Lightweight health probe.
**Auth required:** No
**Response 200:** `{ success, message, endpoints: { shkeeper, decentralized, health } }`
**Response 200:** `{ success, message, endpoints }`. Older builds may still list legacy endpoint names in this health payload; rely on `app.ts` mounts for the authoritative live surface.
### GET /api/payment/shkeeper/config
**Description:** Same payload as `/configuration` but tailored for the SHKeeper-hosted widget; includes explicit CORS `*` headers.
**Description:** Historical compatibility endpoint for the old SHKeeper-hosted widget. It is not part of the current Request Network checkout path.
**Auth required:** No
## Payment records (CRUD)
### POST /api/payment
**Description:** Create a payment record (manual entry — usually the SHKeeper intent path is preferred).
**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/intents`.
**Auth required:** Bearer JWT
**Request body:**
```ts
@@ -139,10 +141,44 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
### POST /api/payment/verify
**Description:** Frontend verification endpoint used by the Web3 flow. Confirms a payment and updates the related [[PurchaseRequest]].
**Description:** Legacy frontend verification endpoint used by the wallet-direct Web3 flow. Confirms a payment and updates the related [[PurchaseRequest]].
**Auth required:** Bearer JWT
## SHKeeper - Pay-in
## Request Network - Pay-in
### POST /api/payment/request-network/intents
**Description:** Creates a Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request.
**Auth required:** Bearer JWT (buyer)
**Request body:**
```ts
{
purchaseRequestId: string;
sellerOfferId: string;
sellerId: string;
amount: number;
token?: string; // default "USDT" or REQUEST_NETWORK_PAYMENT_CURRENCY
network?: string; // default REQUEST_NETWORK_NETWORK or "bsc"
metadata?: Record<string, unknown>;
}
```
**Response 200:** `{ success: true, data: { paymentId, paymentUrl, providerPaymentId, raw, ... } }`
### GET /api/payment/request-network/:paymentId/checkout
**Description:** Rehydrates the in-house checkout payload for an existing Request Network payment so the frontend can build the on-chain approval/payment transaction without relying on the hosted RN page.
**Auth required:** Bearer JWT (buyer who owns the payment)
### POST /api/payment/request-network/webhook
**Description:** Request Network posts settlement updates here. The route verifies `x-request-network-signature` over the raw body, deduplicates delivery IDs, evaluates the Transaction Safety Provider, and coordinates the payment/ledger update.
**Auth required:** No (signature-protected)
**Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature.
## Legacy SHKeeper - Pay-in
> [!warning] Historical route family
> The current `app.ts` mounts Request Network routes, not `services/payment/shkeeper/*`. Keep this section only for legacy record migration and old operational context.
### POST /api/payment/shkeeper/intents
@@ -230,7 +266,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
**Description:** Counters for webhook deliveries (success / failure / duplicates).
**Auth required:** Bearer JWT (admin)
## SHKeeper - Release / Refund (escrow)
## Legacy SHKeeper - Release / Refund (escrow)
These build an admin-signed transaction off-chain and require a follow-up confirm with the broadcast tx hash. Source: `shkeeperService.buildAdminSignedTxPayload` and `confirmAdminTx`.
@@ -258,9 +294,9 @@ These build an admin-signed transaction off-chain and require a follow-up confir
**Auth required:** Bearer JWT (admin)
**Request body:** `{ txHash: string }`
## SHKeeper - Payouts
## Legacy SHKeeper - Payouts
Payouts are SHKeeper-side outbound transfers (admin pays the seller from a hot wallet).
Historical payouts were SHKeeper-side outbound transfers. Current routine releases should use ledger-gated release/refund orchestration instead.
### POST /api/payment/shkeeper/payout
@@ -296,7 +332,7 @@ Payouts are SHKeeper-side outbound transfers (admin pays the seller from a hot w
**Auth required:** No (signature checked)
**Response 200/400:** `{ success, message, data }`
## DePay / Web3 (decentralized)
## Legacy Web3 Wallet-Direct
### POST /api/payment/decentralized/save
@@ -351,7 +387,7 @@ Payouts are SHKeeper-side outbound transfers (admin pays the seller from a hot w
### POST /api/payment/decentralized/admin-payout
**Description:** Pay a seller directly from an admin hot wallet (no SHKeeper).
**Description:** Pay a seller directly from an admin hot wallet. This bypasses the newer ledger-gated release/refund orchestration and should not be used for routine releases.
**Auth required:** Bearer JWT (admin)
**Request body:**
```ts
@@ -459,7 +495,7 @@ Same result shape as above, but for a single destination.
- `completed` - confirmed, escrow funded
- `failed` - intentionally failed (expired, declined, refused)
- `cancelled` - cancelled by user/admin
- `released` - escrow released to seller (`shkeeper` flow)
- `released` - escrow released to seller through the release/refund orchestration and custody signer
- `refunded` - escrow returned to buyer
Escrow state (`escrowState`): `unfunded``funded``released` | `refunded`.
@@ -558,7 +594,7 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded
## Related
- [[Payment Flow]]
- [[Escrow Flow]]
- [[SHKeeper Webhook Flow]]
- [[Request Network Integration Constraints]]
- [[Payout Flow]]
- [[Socket Events]]

View File

@@ -5,7 +5,7 @@ tags: [api, payments, trezor, safekeeping]
# Trezor API
The Trezor API is mounted at `/api/trezor`. It is optional support for hardware-backed safekeeping and does not replace SHKeeper or Request Network.
The Trezor API is mounted at `/api/trezor`. It is optional support for hardware-backed safekeeping and does not replace Request Network checkout, the funds ledger, or the broader Safe/multisig custody roadmap.
Enforcement is controlled by:

View File

@@ -63,7 +63,7 @@ Legacy alternative pay-in path: the buyer connects their own wallet (MetaMask /
- 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.
- Triggers the **same funded-escrow cascade**: 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
@@ -123,8 +123,8 @@ sequenceDiagram
## 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).
- **`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

View File

@@ -72,7 +72,7 @@ Delivery addresses are required before some sellers will accept your offer.
## 3. Connecting a wallet
If you want to pay via **Web3** instead of SHKeeper invoice:
If you want to pay from your own wallet:
1. **Dashboard → Account → Wallet**.
2. Click **Connect Wallet**.
@@ -81,7 +81,7 @@ If you want to pay via **Web3** instead of SHKeeper invoice:
5. The connected address appears as a chip. You can disconnect anytime.
> [!info]
> Connecting a wallet is **optional**. SHKeeper QR payments work without one. See [[Payment Flow - DePay & Web3]].
> Connecting a wallet is required for the in-house Request Network checkout. See [[Escrow Flow]] and [[Request Network Integration Constraints]].
---
@@ -202,32 +202,22 @@ Effects:
## 8. Paying for an order
Two payment paths. Pick at the **Pay** step.
The current payment path is the Request Network in-house checkout.
### 8.1 Path A — SHKeeper invoice (recommended for non-crypto-native users)
### 8.1 Request Network checkout
1. Click **Pay with crypto invoice**.
1. Click **Pay**.
2. Choose a token + network (e.g., USDT on BSC).
3. A QR code + address appears.
4. Open your wallet (any wallet that supports the network).
5. Scan the QR, send the exact amount, confirm in your wallet.
3. Connect or select your wallet.
4. Approve the token spend if prompted.
5. Confirm the payment transaction in your wallet.
6. The page updates in real-time as the blockchain confirms (typically 30s5 min).
7. Status moves to **Funded** when fully confirmed.
> [!warning]
> Send the **exact** amount on the **exact** network. Sending USDT on the wrong network (e.g., ERC-20 instead of BSC) WILL lose your funds. The displayed network is binding.
See [[Payment Flow - SHKeeper]].
### 8.2 Path B — Direct Web3 wallet
1. Click **Pay from connected wallet** (requires a connected wallet — see §3).
2. Your wallet pops up a transaction approval (token transfer to escrow address).
3. Approve & sign.
4. Wait for on-chain confirmation.
5. Backend verifies the transaction and moves status to **Funded**.
See [[Payment Flow - DePay & Web3]].
See [[Escrow Flow]].
---
@@ -405,6 +395,6 @@ Contact support — account deletion is a manual operation by admins to ensure a
## 16. Related
- [[Seller Guide]] · [[Admin Guide]] · [[Support Guide]]
- Flows: [[Authentication Flow]] · [[Registration Flow]] · [[Purchase Request Flow]] · [[Payment Flow - SHKeeper]] · [[Payment Flow - DePay & Web3]] · [[Delivery Confirmation Flow]] · [[Dispute Flow]] · [[Rating Flow]] · [[Referral Flow]]
- Flows: [[Authentication Flow]] · [[Registration Flow]] · [[Purchase Request Flow]] · [[Escrow Flow]] · [[Delivery Confirmation Flow]] · [[Dispute Flow]] · [[Rating Flow]] · [[Referral Flow]]
- Models: [[User]] · [[PurchaseRequest]] · [[Payment]] · [[Address]]
- [[Glossary]]

View File

@@ -190,7 +190,7 @@ Use `src/utils/logger.ts`:
import { log, logError } from "src/utils/logger";
log(`✅ Payment ${id} confirmed`);
logError("SHKeeper webhook verification failed", err);
logError("Request Network webhook verification failed", err);
```
Never use raw `console.error` in service code — it bypasses Sentry breadcrumbs.

View File

@@ -80,36 +80,39 @@ In dev, Redis runs without a password. In production the compose entrypoint is `
---
## Payments — SHKeeper
## Payments — Request Network
SHKeeper is the crypto payment gateway. See [[Payment Flow]] and [[SHKeeper Integration]] in the architecture section.
Request Network is the current primary payment provider. See [[PRD - Request Network In-House Checkout]], [[Request Network Integration Constraints]], and [[Escrow Flow]].
| Name | Repo | Required | Default | Example | Purpose |
|------|------|----------|---------|---------|---------|
| `SHKEEPER_BASE_URL` | backend | ✅ | — | `https://shkeeper.example.com` | Base API URL |
| `SHKEEPER_API_URL` | backend | ✅ | — | `https://shkeeper.example.com/api/v1` | Versioned API URL |
| `SHKEEPER_API_KEY` | backend | ✅ | — | — | `X-Shkeeper-Api-Key` header |
| `SHKEEPER_WEBHOOK_SECRET` | backend | ✅ | — | — | HMAC secret for inbound webhook signatures |
| `SHKEEPER_CALLBACK_SECRET` | backend | ✅ | — | — | Older alias for webhook secret; some payloads still use it |
| `SHKEEPER_ALLOWED_TOKENS` | backend | optional | `USDT,USDC` | `USDT,USDC,BTC` | Comma-separated list of accepted tokens |
| `SHKEEPER_NETWORKS` | backend | optional | `bsc,polygon` | `bsc,polygon,eth` | Networks enabled in checkout |
| `SHKEEPER_ENVIRONMENT` | backend | optional | `production` | `sandbox` | Switches SHKeeper sandbox vs prod behaviour |
| `SHKEEPER_FORCE_PAYOUT_DEMO` | backend | optional | `false` | `true` | Skips real-chain payout; demo-confirms after 5s |
| `SHKEEPER_FORCE_REAL` | backend | optional | `false` | `true` | Forces real-chain even in dev/sandbox |
| `ADMIN_PAYOUT_WALLET_ADDRESS` | backend | ✅ for payouts | — | `0xAc23…` | Wallet that receives platform fees / payouts |
| `REQUEST_NETWORK_ENABLED` | backend | optional | `true` | `true` | Enables `request.network` as an available provider |
| `REQUEST_NETWORK_API_KEY` | backend | ✅ | — | `cli_...` | Request Network API credential |
| `REQUEST_NETWORK_API_BASE_URL` | backend | ✅ | `https://api.request.network` | `https://api.request.network` | Request Network API base URL |
| `REQUEST_NETWORK_ORIGIN` | backend | ✅ | `FRONTEND_URL` | `https://dev.amn.gg` | Origin sent to Request Network API |
| `REQUEST_NETWORK_MERCHANT_REFERENCE` | backend | ✅ | — | `<receiver>@eip155:56#...:<token>` | Encodes receiver, chain, payment reference, and token context |
| `REQUEST_NETWORK_NETWORK` | backend | optional | `bsc` | `bsc` | Default checkout network |
| `REQUEST_NETWORK_PAYMENT_CURRENCY` | backend | optional | `USDT` | `USDC` | Default checkout token symbol |
| `REQUEST_NETWORK_WEBHOOK_CALLBACK_URL` | backend | ✅ | — | `https://dev.amn.gg/api/payment/request-network/webhook` | Provider callback URL |
| `REQUEST_NETWORK_WEBHOOK_SECRET` | backend | ✅ | — | — | HMAC secret for inbound webhook signatures |
| `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` | backend | optional | `false` | `false` | Allows explicit Request Network test webhooks only for controlled smoke tests |
| `ESCROW_WALLET_ADDRESS` | backend | ✅ | — | `0xa304…` | Master escrow address used by payments service |
| `RECEIVER_WALLET_ADDRESS` | backend | optional | — | `0x…` | Used by alternative payout flows |
### Historical SHKeeper keys
`SHKEEPER_*` variables may still appear in legacy migration docs or old `.env` files. They are not the current primary checkout path and should not be used for new payment work unless a deliberate legacy-record reconciliation task requires them.
---
## Payments — Provider Selection
| Name | Repo | Required | Default | Example | Purpose |
|------|------|----------|---------|---------|---------|
| `PAYMENT_PROVIDER` | backend | optional | `shkeeper` | `request.network` | Active provider for new payment intents |
| `PAYMENT_DEFAULT_PROVIDER` | backend | optional | `shkeeper` | `shkeeper` | Fallback alias when `PAYMENT_PROVIDER` is unset |
| `PAYMENT_ENABLED_PROVIDERS` | backend | optional | `shkeeper` | `shkeeper,request.network` | Comma-separated providers allowed at runtime |
| `PAYMENT_ROLLBACK_PROVIDER` | backend | optional | `shkeeper` | `shkeeper` | Provider used when selected provider is not enabled |
| `PAYMENT_PROVIDER` | backend | optional | `request.network` | `request.network` | Active provider for new payment intents |
| `PAYMENT_DEFAULT_PROVIDER` | backend | optional | `request.network` | `request.network` | Fallback alias when `PAYMENT_PROVIDER` is unset |
| `PAYMENT_ENABLED_PROVIDERS` | backend | optional | `request.network` | `request.network` | Comma-separated providers allowed at runtime |
| `PAYMENT_ROLLBACK_PROVIDER` | backend | optional | `request.network` | `request.network` | Provider used when selected provider is not enabled |
| `PAYMENT_PROVIDER_MODE` | backend | optional | `live` | `dry-run` | Provider mode: `live`, `dry-run`, or `read-only` |
| `REQUEST_NETWORK_ENABLED` | backend | optional | `false` | `true` | Adds `request.network` to enabled providers when no explicit list is set |
| `PAYMENT_REQUEST_NETWORK_COHORT_PERCENT` | backend | optional | `0` | `10` | Percent of new checkout cohort eligible for Request Network |
@@ -125,11 +128,11 @@ SHKeeper is the crypto payment gateway. See [[Payment Flow]] and [[SHKeeper Inte
---
## Payments — DePay / Web3 (frontend)
## Payments — Wallet UI (frontend)
| Name | Repo | Required | Default | Example | Purpose |
|------|------|----------|---------|---------|---------|
| `NEXT_PUBLIC_DEPAY_INTEGRATION_ID` | frontend | ✅ for DePay | — | `1330e2d3-…` | DePay widget integration ID |
| `NEXT_PUBLIC_DEPAY_INTEGRATION_ID` | frontend | legacy only | — | `1330e2d3-…` | Historical DePay widget integration ID |
| `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS` | frontend | ✅ | — | `0xa304…` | Escrow address shown to buyers in the wallet flow |
| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | frontend | ✅ | — | `283b54dd…` | WalletConnect v2 project ID |
| `NEXT_PUBLIC_ALCHEMY_API_KEY_MAINNET` | frontend | ✅ | — | — | Alchemy RPC for mainnet |
@@ -271,20 +274,23 @@ SMTP_USER=
SMTP_PASS=
SMTP_FROM="AMN <no-reply@amn.gg>"
# SHKeeper (set when ready)
PAYMENT_PROVIDER=shkeeper
PAYMENT_ENABLED_PROVIDERS=shkeeper
PAYMENT_ROLLBACK_PROVIDER=shkeeper
REQUEST_NETWORK_ENABLED=false
PAYMENT_LEDGER_ENFORCEMENT=false
# Payments
PAYMENT_PROVIDER=request.network
PAYMENT_ENABLED_PROVIDERS=request.network
PAYMENT_ROLLBACK_PROVIDER=request.network
REQUEST_NETWORK_ENABLED=true
REQUEST_NETWORK_API_KEY=
REQUEST_NETWORK_API_BASE_URL=https://api.request.network
REQUEST_NETWORK_ORIGIN=https://dev.amn.gg
REQUEST_NETWORK_MERCHANT_REFERENCE=
REQUEST_NETWORK_NETWORK=bsc
REQUEST_NETWORK_PAYMENT_CURRENCY=USDC
REQUEST_NETWORK_WEBHOOK_CALLBACK_URL=https://dev.amn.gg/api/payment/request-network/webhook
REQUEST_NETWORK_WEBHOOK_SECRET=
REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS=false
PAYMENT_LEDGER_ENFORCEMENT=true
PAYMENT_RECONCILIATION_ENABLED=false
TREZOR_SAFEKEEPING_REQUIRED=false
SHKEEPER_BASE_URL=
SHKEEPER_API_URL=
SHKEEPER_API_KEY=
SHKEEPER_WEBHOOK_SECRET=
SHKEEPER_CALLBACK_SECRET=
SHKEEPER_FORCE_PAYOUT_DEMO=true
# OpenAI (optional)
OPENAI_API_KEY=

View File

@@ -125,7 +125,7 @@ frontend/
│ ├── actions/ # Server-side / shared async API calls (axios)
│ ├── auth/ # JWT / OAuth / passkey context, guards, hooks, services
│ ├── socket/ # Socket.IO client, hooks, components, contexts
│ ├── web3/ # WalletConnect + Alchemy + DePay glue
│ ├── web3/ # WalletConnect + Alchemy + Request Network checkout glue
│ ├── routes/ # Static path constants (paths object)
│ ├── utils/ # logger, format-number, format-time, localStorage, …
│ ├── types/ # Shared TS types (mirrors backend models where useful)

View File

@@ -122,9 +122,9 @@ You must `docker login git.manko.yoga -u manawenuz` first. Pushes both tags and
### `start-ngrok.sh`
**Purpose.** Start `ngrok http` against a local port (default `8083`) and print the public URL by polling the inspector at `127.0.0.1:4040`. Lets you receive SHKeeper webhooks on your laptop.
**Purpose.** Start `ngrok http` against a local port (default `8083`) and print the public URL by polling the inspector at `127.0.0.1:4040`. Lets you receive Request Network webhooks on your laptop.
**When to run.** Local SHKeeper webhook development.
**When to run.** Local Request Network webhook development.
**Example.**
@@ -219,9 +219,9 @@ Each script takes a base URL + admin token. Inspect them before running.
### `manual-test.ts`
**Purpose.** Local sanity check for the SHKeeper service: calls `createPayInIntent` with mock data and verifies a webhook signature in dev mode.
**Purpose.** Historical sanity check for the old SHKeeper service.
**When to run.** Smoke-test after changing SHKeeper code without running the full suite.
**When to run.** Legacy-record troubleshooting only; new payment work should use the Request Network tests.
**Example.**
@@ -243,13 +243,13 @@ npm run dev &
ts-node manual-payout-test.ts
```
> [!warning] Will create a real payout record in the DB. With `SHKEEPER_FORCE_PAYOUT_DEMO=true` no on-chain transaction is sent; without that flag a real on-chain transfer can occur.
> [!warning] Will create a real payout record in the DB. Treat this as a legacy/manual helper; routine releases should go through ledger-gated release/refund orchestration.
### `fix-transaction-hashes.js`
**Purpose.** One-off backfill — walks completed Payments missing `transactionHash`, queries SHKeeper for the original invoice, extracts the confirmed transaction hash, and updates the payment document.
**Purpose.** Historical one-off backfill — walks completed legacy Payments missing `transactionHash`, queries SHKeeper for the original invoice, extracts the confirmed transaction hash, and updates the payment document.
**When to run.** Only if you see payments displayed as "completed" with a missing tx hash. Rate-limits itself with a 1s delay per record.
**When to run.** Only for old SHKeeper records. New Request Network payments should be reconciled through Request Network webhook/reconciliation tooling.
**Example.**
@@ -260,7 +260,7 @@ SHKEEPER_API_KEY=... \
node fix-transaction-hashes.js
```
> [!warning] Hits the live SHKeeper API and writes to MongoDB. Take a backup ([[Backup & Recovery]]).
> [!warning] Hits the live legacy SHKeeper API and writes to MongoDB. Take a backup ([[Backup & Recovery]]).
### `check-templates.js`, `get-admin-token.js`

View File

@@ -47,9 +47,9 @@ Both repos use **Jest** as the unit/integration runner. The frontend additionall
There are also four large aggregate suites referenced in `package.json` (some may live in branches or be reintroduced as the codebase evolves):
- `models.test.ts` — every Mongoose schema, validation, indexes, relationships
- `payment-services.test.ts` — DePay, SHKeeper, Web3, admin operations
- `request-network-adapter.test.ts`, `request-network-webhook.test.ts`, `rn-in-house-checkout.test.ts` — Request Network checkout and webhook behavior
- `payment-ledger.service.test.ts`, `payment-release-refund-orchestration.test.ts` — ledger and release/refund behavior
- `complete-backend.test.ts` — Auth, marketplace, chat, notification, address, user, file, email, AI
- `shkeeper-backend.test.ts` — Service layer + API endpoints for SHKeeper
### Commands
@@ -61,11 +61,11 @@ npm run test:watch # interactive watch mode
npm run test:coverage # also emit coverage report to ./coverage/
npm run test:all # explicit __tests__/ folder
# Focused suites (each maps to a single file):
# Focused suites:
npm run test:models # jest __tests__/models.test.ts
npm run test:payment # jest __tests__/payment-services.test.ts
npm run test:complete # jest __tests__/complete-backend.test.ts
npm run test:shkeeper # jest __tests__/shkeeper-backend.test.ts
npm run test -- --testPathPattern=request-network
npm run test -- --testPathPattern=payment-ledger
```
Pass extra Jest flags after `--`:
@@ -101,7 +101,7 @@ describe('GET /api/health', () => {
```
3. Use the in-memory DB — connections are wired in `setup.ts`. Each test starts with a clean collection.
4. Mock outbound HTTP (SHKeeper, OpenAI) with `jest.spyOn(axios, 'post')`. Never hit a real provider from tests.
4. Mock outbound HTTP (Request Network, OpenAI, AML providers) with `jest.spyOn(axios, 'post')` or a dedicated adapter mock. Never hit a real provider from tests.
> [!warning] `maxWorkers: 1` makes tests serial. Don't introduce timing-sensitive parallelism — instead, keep individual tests small and deterministic.

View File

@@ -9,6 +9,9 @@ created: 2026-05-24
These runbooks cover the selected backend/funds architecture defined in
[[Backend Core Stack Decision Record - 2026-05-24]].
> [!note] Historical migration context
> Sections that mention keeping SHKeeper active describe the migration period from the old payment rail to Request Network. Current new-payment operations should use [[Escrow Flow]], [[Request Network Integration Constraints]], and [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
## 1. Migration runbook (legacy + provider migration)
### 1.1 Preflight

View File

@@ -44,11 +44,25 @@ base: {
- Restored Base USDC/USDT entries to `tokens.json`.
- All 5 chains now active in the registry.
## AML scope note (for legal / compliance review)
The current AML implementation (Task #10, shipped in backend/frontend 2.6.47) performs **sanctions-only screening** via the Chainalysis Public Sanctions API. It checks whether a buyer's source wallet address appears on known sanctions lists (OFAC, UN, HMT, etc.). It does **not** perform full AML risk scoring — there is no transaction clustering, entity attribution, travel-rule monitoring, or behavioral risk scoring. Upgrading to comprehensive AML/KYT would require a paid Chainalysis KYT tier (or equivalent provider such as Elliptic, TRM Labs, or ComplyAdvantage), which runs ~$100K+/year for production volumes and requires an enterprise contract. The sanctions-only tier is free (5,000 requests per 5 minutes) and is the correct scope for a v1 compliance posture, but it should be explicitly described to regulators/customers as "sanctions screening" rather than "AML screening."
## Remaining work
- [ ] BSC USDT paid end-to-end probe (PRD §2 AC #3) — **pending human-in-the-loop**.
- [x] Mainnet USDT `approve(0)` reset verification (PRD §2 AC #4) — **VERIFIED via anvil fork test**.
## Human-blocked items (requires owner with wallet on dev)
These three items cannot be validated by automated tests alone. A human with a funded wallet on the dev environment must execute each probe before the corresponding feature is considered production-ready.
| # | Item | Precise next step | Blocking |
|---|---|---|---|
| 1 | **Task #7C — Live multi-seller divergent-destination probe** | Create a cart with seller-offers from ≥2 different sellers, complete checkout, verify RN creates 2 separate Payments with 2 distinct derived destination addresses, and both webhooks fire correctly. | Task #7 closure |
| 2 | **Task #8 — BSC USDT paid end-to-end probe** | On dev.amn.gg, complete a real BSC USDT pay-in through the in-house checkout (approve + `transferFromWithReferenceAndFee`), confirm webhook marks Payment `completed`, and BscScan shows the token transfer. | Multichain release gate |
| 3 | **Task #11 — Trezor signing dry-run** | Register a physical Trezor via `/api/trezor/register`, build a sweep tx via `POST /api/admin/actions/build-tx`, sign it on-device through the admin UI, broadcast via wagmi, and confirm `POST /api/admin/actions/confirm-tx` accepts the Trezor proof. | Trezor enforcement toggle |
## Mainnet USDT approve(0) reset — fork test verification
**Test:** `scripts/tenderly-usdt-reset-test.sh` (anvil fork of Ethereum mainnet)

View File

@@ -0,0 +1,217 @@
# Task #11 Pre-flight Inventory — Trezor Signing for Admin Actions
> Status: **Findings / design review** — do not implement until human probes for #7C and #8 are complete.
> Date: 2026-05-28
> Scope: Hardware-wallet signing for sweep, release, and refund admin actions. Backend already has xpub derivation, registration, and message-formatting infrastructure. This inventory covers what is **missing** on the frontend and what the end-to-end flow looks like.
---
## 1. Library choice: `@trezor/connect-web`
### Option matrix
| Library | Maturity | Browser support | Bundle size | Recommendation |
|---|---|---|---|---|
| `@trezor/connect-web` | Official, actively maintained by SatoshiLabs | Chrome/Edge/Brave (WebUSB); Firefox requires Trezor Bridge | ~200 KB compressed | **✅ Use this** |
| `trezor-connect` (legacy) | Deprecated, v8 frozen | Same as above | Larger | ❌ Do not use — no longer updated |
| `@trezor/connect` (node/headless) | For server-side or Electron | N/A (no browser popup) | Smaller | ❌ Wrong environment — we need browser UI |
### Why `@trezor/connect-web`
- The Trezor team consolidated on `@trezor/connect-web` as the single browser SDK. It injects a secure iframe from `https://connect.trezor.io/<version>/iframe.html` and opens a trusted popup for device interaction.
- **WebUSB** works on Chromium-based browsers (Chrome, Edge, Brave, Arc) without any native software. **Firefox** falls back to Trezor Bridge, which most admin users already have installed via Trezor Suite.
- The API surface is promise-based and Typescript-friendly:
```ts
import TrezorConnect from '@trezor/connect-web';
await TrezorConnect.init({
lazyLoad: true,
manifest: { email: 'dev@amn.gg', appUrl: 'https://dev.amn.gg' },
});
const result = await TrezorConnect.ethereumSignTransaction({
path: "m/44'/60'/0'/0/0",
transaction: {
to: '0x...',
value: '0x0',
gasPrice: '0x...',
gasLimit: '0x...',
nonce: '0x...',
chainId: 56,
data: '0x...', // ERC-20 transfer or contract call
},
});
```
- **Mobile is out of scope** — WebUSB does not work on iOS Safari, and Android support is spotty. Admin actions are desktop-only by design.
### Installation
```bash
cd frontend && npm install @trezor/connect-web
```
---
## 2. Dev vs. prod signing flow — end-to-end
### Current state (backend already shipped)
The backend has a complete `TrezorAccount` model, xpub-based HD derivation, registration challenge/response, and operation-message formatting. The `releaseRefundService` already calls `assertTrezorSignatureForOperation()` when `TREZOR_SAFEKEEPING_REQUIRED=true`. The `sweepService` has a `SweepSigner` abstraction with a `HotKeySweepSigner` and a `BuildOnlySigner` (returns the tx without signing). What is missing is a `TrezorSweepSigner` and the frontend connector.
### Proposed dev/prod flow
#### Step A — Admin registers Trezor (already works backend-only)
1. Admin opens `/dashboard/admin/trezor-register`.
2. Frontend calls `TrezorConnect.getPublicKey({ coin: 'ETH', path: "m/44'/60'/0'/0" })`.
3. Device shows popup; admin confirms.
4. Frontend receives `xpub` + first derived address (`m/44'/60'/0'/0/0`).
5. Frontend calls `GET /api/trezor/registration-message?xpub=...&registrationAddress=...`.
6. Frontend calls `TrezorConnect.signMessage({ path: "m/44'/60'/0'/0/0", message: <challenge>, coin: 'ETH' })`.
7. Frontend `POST /api/trezor/register` with xpub, registration address, proof message, and proof signature.
8. Backend verifies and stores the account.
#### Step B — Admin triggers a sweep/release/refund
1. Admin opens `/dashboard/admin/sweeps` (or release/refund UI) and clicks "Execute sweep" on a pending destination.
2. Frontend calls `POST /api/admin/actions/build-tx` (new endpoint needed) with:
```json
{ "action": "sweep", "destinationId": "...", "chainId": 56 }
```
3. Backend builds the unsigned transaction (same logic as `BuildOnlySigner`), estimates gas, computes nonce, and returns:
```json
{
"unsignedTx": {
"to": "0x...",
"data": "0x...",
"value": "0x0",
"gasLimit": "0x...",
"gasPrice": "0x...",
"nonce": 42,
"chainId": 56
},
"derivationPath": "m/44'/60'/0'/0/7",
"txIntentHash": "0x..."
}
```
4. Frontend displays a confirmation modal showing:
- From address (derived from xpub at the returned path)
- To address
- Token + amount
- Network
- Gas estimate
5. Admin clicks "Sign with Trezor".
6. Frontend calls `TrezorConnect.ethereumSignTransaction({ path, transaction: unsignedTx })`.
7. Device shows popup with tx details; admin physically confirms on device.
8. Frontend receives the signed transaction bytes (`result.payload.serializedTx`).
9. Frontend broadcasts via wagmi's `sendTransaction({ raw: serializedTx })` or ethers `provider.broadcastTransaction(serializedTx)`.
10. After broadcast, frontend calls `POST /api/admin/actions/confirm-tx` with:
```json
{
"action": "sweep",
"destinationId": "...",
"txHash": "0x...",
"trezor": {
"message": "Amanat escrow Trezor transaction approval\n...",
"signature": "0x..."
}
}
```
11. Backend verifies the Trezor signature against the registered xpub, appends the ledger entry, and marks the sweep complete.
### Key design decisions to review
| Decision | Option A (recommended) | Option B |
|---|---|---|
| **Who broadcasts?** | Browser (wagmi/ethers) — backend never sees raw signed bytes | Backend receives signed tx and broadcasts |
| **Why A?** | Backend holding a signed tx is almost as sensitive as holding a private key. Browser broadcast keeps the signature in userland. | Simpler for unreliable browser networks, but increases backend attack surface. |
| **Message signing vs tx signing** | Use `ethereumSignTransaction` for actual sweeps; use `signMessage` for the registration proof and for release/refund operation intents | Use `signMessage` for everything — but then backend must reconstruct and verify the tx hash, which is fragile |
| **Derivation path discovery** | Backend tells frontend which path to use (from `DerivedDestination` record). Frontend does not iterate. | Frontend derives addresses from xpub locally to find the right one — more client-side code, more exposure |
---
## 3. Admin UI surface needed
### New pages / sections
| Route | Purpose | Admin role |
|---|---|---|
| `/dashboard/admin/trezor-register` | Register a Trezor xpub, verify first derived address, label device | `superadmin` |
| `/dashboard/admin/trezor-status` | Show registered device, xpub fingerprint, derived addresses in use, last activity | `superadmin` |
| `/dashboard/admin/sweeps` | List pending derived destinations awaiting sweep; "Build tx" → "Sign with Trezor" → "Broadcast" flow | `admin` |
| `/dashboard/admin/pending-actions` | **NEW** — unified queue of all actions awaiting Trezor signature (sweeps, releases, refunds). Shows who requested, when, amount, and a "Sign now" button. | `admin` |
### `/dashboard/admin/pending-actions` — the critical new UI
This is the biggest gap. Today, sweeps are either cron-fired or triggered ad-hoc. With Trezor, every sweep becomes a human-in-the-loop action because the device must be present to sign. The admin needs a queue.
**Proposed UI elements:**
1. **Pending queue table**
- Columns: Action type (sweep / release / refund), Payment/Destination ID, Amount + token, Chain, Requested by, Requested at, Status (`pending_signature` / `signed_broadcasting` / `confirmed` / `failed`)
- Row actions: "View tx details", "Sign with Trezor", "Cancel" (superadmin only)
2. **Tx detail modal**
- Shows the unsigned tx JSON in human-readable form (from, to, token, amount, gas)
- Shows the derivation path and how it maps to the registered Trezor
- "Sign with Trezor" button → triggers `@trezor/connect-web` flow
3. **Signing state machine**
- `idle` → `building_tx` → `awaiting_device` (popup open) → `signing` (user confirming on device) → `broadcasting` → `confirmed` / `failed`
- Each state shows a distinct UI indicator so the admin knows the device is waiting for them
4. **Break-glass override**
- A "Use hot-key override" button visible only to `superadmin`
- Clicking it shows a warning: "This bypasses Trezor safekeeping and triggers a Telegram alarm. Are you sure?"
- If confirmed, frontend calls `POST /api/admin/actions/break-glass` which toggles hot-key signing for 1 hour and sends alarm
### Components to build (frontend)
```
frontend/src/sections/admin/trezor/
trezor-register-view.tsx # Registration flow
trezor-status-view.tsx # Device status + derived addresses
pending-actions-view.tsx # Queue of actions awaiting signature
trezor-sign-modal.tsx # Tx detail + sign button + state machine
hooks/
useTrezorConnect.ts # Wraps @trezor/connect-web init + methods
useTrezorSignTransaction.ts # Handles ethereumSignTransaction flow
usePendingActions.ts # Polls /api/admin/pending-actions
```
---
## 4. Backend gaps to fill (minor)
The backend is ~70% complete for Trezor. Remaining work:
| Gap | Effort | Notes |
|---|---|---|
| `POST /api/admin/actions/build-tx` | Small | Reuses `BuildOnlySigner` logic; returns unsigned tx + derivation path |
| `POST /api/admin/actions/confirm-tx` | Small | Reuses existing `releaseRefundService` / sweep confirmation; adds Trezor proof verification |
| `POST /api/admin/actions/break-glass` | Small | Toggles env override for 1h, sends Telegram alarm, logs audit entry |
| `GET /api/admin/pending-actions` | Small | Queries `DerivedDestination` (status=`awaiting_sweep`) + Payment (status=`awaiting_release`/`awaiting_refund`) |
| `TrezorSweepSigner` class | Small | Implements `SweepSigner` interface; instead of signing, it queues the action and returns a "pending signature" result |
| Admin authorization on new routes | Tiny | Reuse existing `authorizeRoles(['admin', 'superadmin'])` |
---
## 5. Risk notes
- **WebUSB reliability**: Some users report `Transport_Missing` errors even on Chrome when the Trezor Bridge is also installed. The fix is to uninstall Bridge and rely purely on WebUSB, or to ensure the Bridge daemon is running. We should document this in the admin setup guide.
- **Trezor Model One vs Model T vs Safe 3/5**: `@trezor/connect-web` abstracts all models. The only visible difference is whether the user confirms on buttons (Model One) or touchscreen (Model T/Safe). No code change needed.
- **Passphrase wallets**: If the admin uses a passphrase-protected hidden wallet, the passphrase must be entered in the Trezor popup. Our code does not need to handle this — it's part of the SDK popup flow.
- **Multi-admin (m-of-n)**: Out of scope for v1. The current `TrezorAccount` model stores one xpub per user. A future v2 could store multiple registered devices and require `t` signatures before `confirm-tx` succeeds. The `pending-actions` queue UI is designed to accommodate this (shows "1 of 2 signatures collected").
---
## 6. Suggested acceptance criteria (for implementation PR)
- [ ] Admin can register a Trezor and `/api/trezor/account` returns `registered: true`
- [ ] Admin can view a pending-actions queue with ≥1 sweep/release/refund awaiting signature
- [ ] Clicking "Sign with Trezor" opens the Trezor popup, displays the tx, and returns a signature
- [ ] Signed tx is broadcast from the browser and hash is reported to backend
- [ ] Backend verifies Trezor proof before confirming the action
- [ ] Break-glass toggle works and fires Telegram alarm
- [ ] Audit log captures: admin user, Trezor address, tx hash, before/after escrow state
- [ ] Without Trezor proof and with `TREZOR_SAFEKEEPING_REQUIRED=true`, release/refund/sweep is rejected