- 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
218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# 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
|