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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
217
08 - Operations/Task 11 Pre-flight Inventory.md
Normal file
217
08 - Operations/Task 11 Pre-flight Inventory.md
Normal 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=...®istrationAddress=...`.
|
||||
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
|
||||
Reference in New Issue
Block a user