Files
nick-doc/08 - Operations/Task 11 Pre-flight Inventory.md
Siavash Sameni 81625d35d2 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
2026-05-28 20:42:42 +04:00

12 KiB

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:
    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

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:
    { "action": "sweep", "destinationId": "...", "chainId": 56 }
    
  3. Backend builds the unsigned transaction (same logic as BuildOnlySigner), estimates gas, computes nonce, and returns:
    {
      "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:
    {
      "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

    • idlebuilding_txawaiting_device (popup open) → signing (user confirming on device) → broadcastingconfirmed / 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