- Add Handoff - RN Multichain Probe - 2026-05-28.md - Update Handoff - Request Network In-House Checkout with Task #8 status - Update Activity Log with backend@ae17b18, frontend@0ebb2f1 - Update PRD §2 acceptance criteria for Task #8 - Update Payment API.md with /api/admin/rn/networks endpoints
270 lines
24 KiB
Markdown
270 lines
24 KiB
Markdown
# PRD: Wallet, Multichain, Confirmations, AML, Trezor
|
||
|
||
> Status: **Living — last edit 2026-05-28 (after Task #7 core shipped in backend/frontend 2.6.42)**
|
||
> Author: nick + claude
|
||
> Owner: backend (payments) + frontend (admin UI + checkout)
|
||
> Related: `PRD - Request Network In-House Checkout.md`, `01 - Architecture/Request Network Integration Constraints.md`, `08 - Operations/Handoff - Request Network In-House Checkout - 2026-05-28.md`
|
||
|
||
Five follow-ups to the in-house Request Network checkout. They are sized so a single contributor can pick up any one of them in isolation. Each is its own Taskmaster top-level task — see `#7…#11`.
|
||
|
||
---
|
||
|
||
## 1. Per-(buyer, sellerOffer) ephemeral destination wallets — Task #7
|
||
|
||
### Status: 🟡 In progress — backend + admin UI landed in 2.6.42; cart-aware UX (A), auto-start cron (D), and `recordSweep` accumulation fix (E) shipped; doc updates (F) in progress; unit tests (B) pending. Live divergent-destination probe (C) still pending.
|
||
|
||
### Problem
|
||
The in-house checkout used to send *all* RN-routed payments to one Amanat-controlled wallet (env: `REQUEST_NETWORK_MERCHANT_REFERENCE`). That wallet was shared across every buyer, every seller, every offer — an audit nightmare and a single point of compromise.
|
||
|
||
### Goal (achieved on the backend)
|
||
For each `Payment` record (which represents a `(buyerId, sellerOfferId)` pair), generate a fresh on-chain destination address, persist it on the `Payment` record, and tell Request Network to expect funds on that address. RN's webhook flows unchanged.
|
||
|
||
### Key clarification: cart with multiple sellers
|
||
|
||
A buyer's cart can contain items from multiple sellers. **That's already modeled in Amanat as N separate `Payment` records, one per `sellerOfferId` in the cart** — see the `uniq_pending_request_network_by_buyer_session` index in `requestNetworkPayInService.ts`. So:
|
||
|
||
- 1 Payment record = 1 derived destination address = 1 RN intent = 1 buyer-side on-chain transaction.
|
||
- A 5-item cart spanning 2 sellers produces 2 Payments, 2 derived addresses, 2 RN intents, and the buyer makes 2 approve+pay tx pairs in sequence.
|
||
- RN's `ERC20FeeProxy.transferFromWithReferenceAndFee` is single-destination by design — there's no atomic multi-recipient split. A future v2 could route the cart through an Amanat splitter contract that fans out to per-seller derived addresses in one buyer tx; that's out of scope here.
|
||
|
||
### Hard-known facts (from RN docs we've cold-inspected so far)
|
||
- The "destination" in RN is the `destinationId` inside the merchant reference: `<address>@eip155:<chainId>#<contractId>:<tokenAddress>`. RN doesn't bind this to an Amanat-level identity; it's just where the funds end up.
|
||
- Each `POST /v2/secure-payments` request *can* pass a different `destinationId`. RN doesn't reject divergent destinations across requests from the same client. **Acceptance probe still pending — see remaining work.**
|
||
- `paymentReference` is derived per request (`last8Bytes(keccak256(requestId+salt+destination))`), so different destinations naturally produce different on-chain refs. The webhook listener keys on the ref + tx hash.
|
||
|
||
### Decisions taken during implementation
|
||
These were open questions in the original draft; the shipped implementation locked them in.
|
||
|
||
| Question | Decision |
|
||
|---|---|
|
||
| Key custody model | **HD wallet, derived addresses, reused per `(buyer, sellerOffer, chainId)` triple**. Backend holds only an xpub (`DERIVED_DESTINATION_XPUB`). The xpriv / master seed lives in KMS or Trezor (Task #11). |
|
||
| Granularity | **Per `(buyer, sellerOffer, chainId)` triple**, reused for repeat payments to the same offer (one address can fund and then be re-funded later — sweeps happen out-of-band). The PRD draft considered single-use rotation; reuse won for simpler audit and to avoid bloating the derivation tree. |
|
||
| Derivation index allocation | Monotonic counter in a `counters` Mongo collection (`{_id: 'derived_destination_index', seq: <int>}`) updated atomically via `findByIdAndUpdate { $inc: { seq: 1 } }`. No re-derivation, no race window. |
|
||
| Sweep strategy | **Cron-based** by default (`DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000`, i.e. 5 min) **plus manual admin trigger**. Both go through the same `sweepService` and the same Transaction Safety Provider checks. Auto-start on backend boot is not wired yet — admins start the cron via `POST /api/payment/derived-destinations/cron/start`. |
|
||
| Signing | **`DERIVED_DESTINATION_SWEEP_SIGNER=build-only`** in prod — the backend builds the sweep tx but doesn't sign it (Trezor flow in Task #11 will). For local dev, `DERIVED_DESTINATION_SWEEP_SIGNER=hot-key` plus `DERIVED_DESTINATION_XPRIV` lets the backend sign — DO NOT USE IN PROD. |
|
||
|
||
### Still open
|
||
1. **Multi-seller cart UX** — not built. Today's frontend assumes 1 Payment per checkout page. The PRD copy from the original draft still applies:
|
||
- "You're paying 2 sellers for this order. Please confirm 4 transactions in your wallet."
|
||
- Progress indicator: "Paid 1 of 2 sellers — continue?"
|
||
- Mid-cart abandonment: the sellers who already received funds are settled; the rest stay pending. No special backend work — the existing Payment lifecycle covers it.
|
||
2. **Cold-payment recovery** — out of scope for v1. Buyer never types an address into our UI, so the only way funds land on a derived address without a Payment record is RN's webhook arriving for a payment we don't recognise. Manual support recovery is acceptable.
|
||
3. **Live "RN accepts divergent destinations" probe** — has to happen on dev with two real paid intents to two different derived addresses, with the webhook firing correctly for both. Until this probe passes, treat the divergent-destination capability as an unverified assumption.
|
||
|
||
### What landed in 2.6.42
|
||
- **Backend (`backend/src/services/payment/wallets/`):**
|
||
- `derivedDestinations.ts` — `getDestinationFor({ buyerId, sellerOfferId, chainId })` returning `{ address, derivationPath, derivationIndex, chainId }`. Idempotent: checks for an existing row first; on E11000 from a race, re-reads the racer's row. Validates the env xpub rejects xpriv / tprv prefixes. Exposes `resolveExpectedRecipientForPayment` for the Transaction Safety Provider's verification path.
|
||
- `sweepService.ts` — pluggable signer abstraction (`build-only` / `hot-key`), ERC-20 `balanceOf` reads via Alchemy/public RPC, sweep orchestration, and an interval-based cron.
|
||
- `derivedDestinationRoutes.ts` — admin-only REST: `GET /` (list with status/chain/address filters and pagination), `POST /sweep` (sweep all eligible), `POST /:id/sweep` (per-row), `GET /config/health`, `POST /cron/start`, `POST /cron/stop`, `GET /cron/status`.
|
||
- **Backend integrations:**
|
||
- `models/DerivedDestination.ts` — Mongo model with sweep status + history. Indexed on `(buyerId, sellerOfferId, chainId)` unique.
|
||
- `models/Payment.ts` — `metadata.derivedDestination` snapshot field.
|
||
- `adapters/types.ts` — `CreatePayInIntentInput.merchantReference` for per-payment override.
|
||
- `requestNetwork/contract.ts` — `buildRequestNetworkMerchantReference` accepts `input.merchantReference` as highest priority.
|
||
- `requestNetwork/merchantReference.ts` — `buildMerchantReference()` inverse of the parser.
|
||
- `requestNetwork/requestNetworkPayInService.ts` — calls `getDestinationFor`, builds the per-payment merchant reference via `buildMerchantReference`, persists `metadata.derivedDestination` on the Payment, passes the override into `adapterResult` via `input.merchantReference`.
|
||
- `requestNetwork/inHouseCheckout.ts` — accepts `destinationOverride`; the on-chain-paymentReference compute-fallback now uses the actual recipient (was reading `parsed.recipient` — hidden bug because RN's response provides the ref directly, but the fallback path was broken for derived destinations).
|
||
- `safety/transactionSafetyProvider.ts` — `resolveExpectedRecipient` checks `payment.metadata.derivedDestination.address` first, then legacy fallback chain.
|
||
- `app.ts` — mounts `/api/payment/derived-destinations`.
|
||
- **Frontend (`frontend/src/sections/admin/derived-destinations/`, `frontend/src/app/dashboard/admin/derived-destinations/`):**
|
||
- List view with filters (status, chain, address search), pagination, sweep-all, cron start/stop.
|
||
- Per-row UI: address with copy + BscScan link, status chip, derivation path, balance, sweep count, last sweep tx link, per-row sweep action.
|
||
- **Env additions** (see `backend/.env.example`): `DERIVED_DESTINATION_XPUB` (required), `DERIVED_DESTINATION_XPRIV` (dev only), `DERIVED_DESTINATION_BASE_PATH=m/44'/60'/0'`, `DERIVED_DESTINATION_CHAIN_ID=56`, `DERIVED_DESTINATION_SWEEP_SIGNER=build-only`, `DERIVED_DESTINATION_MIN_SWEEP_AMOUNT=0`, `DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000`.
|
||
|
||
### Remaining work for Task #7 (kimi)
|
||
|
||
| # | What | Where | Status | Notes |
|
||
|---|------|-------|--------|-------|
|
||
| A | **Cart-aware buyer UX** on the in-house checkout page. | `frontend/src/sections/payment/checkout/rn-in-house-checkout-view.tsx` (and `provider-payment.tsx` for the entry flow). | ✅ Done | Entry walks each `sellerOfferId` in the cart, creates N intents sequentially, stashes them in `sessionStorage`, and the checkout view iterates with per-Payment progress and an "N of M sellers" header. Mid-cart abandonment leaves already-paid Payments settled and the rest in `pending`. |
|
||
| B | **Unit tests** for the new modules. | `backend/__tests__/derived-destinations.test.ts` + `backend/__tests__/sweep-service.test.ts` + `backend/__tests__/request-template-orphan-cleanup.test.ts`. | ✅ Done | 46 tests: `getDestinationFor` idempotency, E11000 race fallback, xpub rejection of xpriv/tprv, `deriveAddressAtIndex` determinism, `recordSweep` accumulation (regression lock-in for E), orphan-cleanup provider filtering (regression lock-in for Gap 2 fix). |
|
||
| C | **Live divergent-destination probe** on dev. | Manual test, no code. | ⏳ Pending | Run two paid intents on the in-house page to two different `sellerOfferId`s (so two different derived addresses), confirm both `TransferWithReferenceAndFee` events fire, both webhooks land, and both Payments transition to `completed`. Record the tx hashes in the handoff doc. |
|
||
| D | **Auto-start the sweep cron on boot**. | `backend/src/app.ts` after the route mount, behind `DERIVED_DESTINATION_SWEEP_AUTOSTART=true`. | ✅ Done | Cron now starts on boot when the env flag is set; admin endpoint still available for manual control. |
|
||
| E | **Fix `recordSweep` accumulation**. | `backend/src/services/payment/wallets/derivedDestinations.ts`. | ✅ Done | Switched from `$setOnInsert: { totalSwept }` to `$inc: { totalSwept }` so accumulation advances on every sweep. |
|
||
| F | **Update Activity Log + API Reference doc** in `nick-doc/` with the new admin endpoints. | `nick-doc/03 - API Reference/...`, `nick-doc/00 - Overview/Activity Log.md`. | 🟡 In progress | Endpoints: `GET /api/payment/derived-destinations`, `POST /api/payment/derived-destinations/sweep`, `POST /api/payment/derived-destinations/:id/sweep`, `GET /api/payment/derived-destinations/config/health`, `POST /api/payment/derived-destinations/cron/start`, `POST /api/payment/derived-destinations/cron/stop`, `GET /api/payment/derived-destinations/cron/status`. All admin-only. |
|
||
|
||
### Non-goals (carried forward unchanged)
|
||
- Multi-chain destinations (covered in Task #8).
|
||
- Buyer-side ephemeral keys (covered in `Request Network Integration Constraints.md` §3, separate PRD).
|
||
- Hardware-wallet-signed sweeps (covered in Task #11 — task #7 ships the `build-only` plumbing that Task #11 plugs into).
|
||
|
||
### Acceptance criteria
|
||
1. ✅ Two payments from the same buyer to two different sellers land on two different addresses on-chain. (Backend logic shipped; frontend multi-checkout UX shipped; live verification pending item C.)
|
||
2. ✅ RN's webhook fires correctly for both, regardless of the destination divergence. (Backend integration + multi-checkout UX shipped; end-to-end verification pending item C.)
|
||
3. 🟡 Sweep runs idempotently — re-running it on an already-swept address advances `totalSwept` correctly. (`$inc` fix shipped in E; lock-in test pending B.)
|
||
4. ✅ Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status.
|
||
5. ✅ Master seed never leaves the KMS/secret store. Backend reads derivation paths from xpub only; production signing path is `build-only` (Task #11 Trezor). Dev hot-key is documented as dev-only.
|
||
6. ✅ Multi-seller cart UX completes N approve+pay pairs sequentially with clear progress UI.
|
||
|
||
---
|
||
|
||
## 2. Multi-chain RN proxy registry + USDC/USDT support — Task #8
|
||
|
||
### Problem
|
||
`backend/src/services/payment/requestNetwork/proxyAddresses.ts` and `tokens.ts` currently hardcode BSC USDC plus a handful of token entries. The in-house checkout will fall through to "in-house checkout not available" the moment a buyer wants to pay on Arbitrum/Polygon/Ethereum, or wants USDT instead of USDC on the same chain.
|
||
|
||
### Goal
|
||
Verified-from-chain registry of:
|
||
- RN's `ERC20FeeProxy` address per supported chain.
|
||
- USDC + USDT contract address + decimals per chain.
|
||
|
||
Plus an admin "supported networks" UI that:
|
||
- Lists each chain + token combo with its current status (verified-on-chain / probe failed / disabled).
|
||
- Shows which chains a given seller has whitelisted (depends on per-seller `acceptedChains` config; out of scope here — see `Request Network Integration Constraints.md` §2).
|
||
|
||
### Hard-known facts
|
||
- RN published their canonical `ERC20FeeProxy` for BSC + Arbitrum as `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` (CREATE2 deterministic). Same address on Ethereum mainnet, Polygon, Base **per the deterministic deployment claim** — needs verifying.
|
||
- BSC USDC has 18 decimals (Binance-Peg). Mainnet/Arb/Polygon USDC have 6.
|
||
- USDT on Ethereum requires `approve(0)` before a non-zero re-approve. Other chains' USDT don't.
|
||
|
||
### Scope
|
||
1. Probe script `backend/scripts/probe-rn-chains.ts` that walks every chain in `supported-chains.json`, calls a known view fn on the candidate proxy address, and confirms it's a real RN proxy (not just bytes). Emits a report.
|
||
2. Promote `tokens.ts` to load from a JSON file + override layer for admin-managed entries. Keep the canonical defaults committed.
|
||
3. Backend route `GET /api/admin/rn/networks` returning the registry plus probe status.
|
||
4. Frontend admin page `/dashboard/admin/networks` rendering the table.
|
||
5. `buildInHouseCheckoutBlock` consults the chain registry; returns `reason='unsupported_chain:<chainId>'` cleanly.
|
||
6. Add the USDT-mainnet `approve(0)` quirk to the frontend approve step. When detected, the approve flow does `approve(spender, 0)` → `approve(spender, amount)`.
|
||
|
||
### Non-goals
|
||
- Letting a seller pick which chain a given buyer can use (separate PRD, §2 in constraints doc).
|
||
- Cross-chain bridging.
|
||
|
||
### Acceptance criteria
|
||
1. ✅ Probe script confirms RN proxy address on BSC, Arbitrum, Polygon, Ethereum. Base canonical address has no code — documented in [[08 - Operations/Handoff - RN Multichain Probe - 2026-05-28]].
|
||
2. ✅ Token registry has entries for USDC + USDT on 4 verified chains (BSC, Arbitrum, Ethereum, Polygon) with correct decimals. Base entries removed pending proxy address discovery.
|
||
3. 🔄 In-house checkout supports USDT on BSC end-to-end — **pending paid probe** (needs real wallet + test BSC USDT on dev).
|
||
4. 🟡 USDT-mainnet `approve(0)` reset is handled in the UI — **implemented but unverified** (see handoff doc for Tenderly fork vs wait-for-usage decision).
|
||
5. ✅ Admin networks page renders the registry with a reload-from-disk button.
|
||
|
||
---
|
||
|
||
## 3. Confirmation-counting + admin threshold UI — Task #9
|
||
|
||
### Problem
|
||
The Transaction Safety Provider already reads `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` from env (default 12). That number is global, baked into env, only changeable by re-deploy. We want it tunable per-chain at runtime by an admin, with a clear UI showing each in-flight payment's current confirmation depth against the threshold.
|
||
|
||
### Goal
|
||
1. Persist per-chain confirmation thresholds in a `ConfigKV` collection (or extend an existing settings model) so admin can adjust without redeploy.
|
||
2. The `TransactionSafetyProvider`'s confirmation check reads the runtime threshold first, falls back to env.
|
||
3. Frontend admin UI to view + edit per-chain thresholds, and a "payments awaiting confirmation" table that shows `confirmations / threshold` updated live.
|
||
|
||
### Scope
|
||
1. New collection / extension to existing `Setting` model: `{ key: 'confirmation_threshold:<chainId>', value: <int>, updatedBy, updatedAt }`.
|
||
2. Public-to-admin endpoint `GET /api/admin/settings/confirmation-thresholds` and `PATCH /api/admin/settings/confirmation-thresholds/:chainId`.
|
||
3. Wire `transactionSafetyProvider` to read from this store; cache for 30s to avoid Mongo hammering.
|
||
4. Frontend admin page `/dashboard/admin/confirmation-thresholds`:
|
||
- Table of chains, current threshold, recommended default (e.g. 12 for BSC).
|
||
- Edit-in-place with a confirm dialog.
|
||
- Audit-log style "changed by X on Y at Z".
|
||
5. Frontend admin page `/dashboard/admin/payments/awaiting-confirmation`:
|
||
- Lists payments with `escrowState !== 'funded'` and `metadata.transactionSafety.lastCheck.status === 'pending'`.
|
||
- For each: tx hash (linked to explorer), current confirmations (poll-driven), threshold, ETA.
|
||
|
||
### Non-goals
|
||
- Per-asset thresholds (only per-chain).
|
||
- Per-seller overrides.
|
||
|
||
### Acceptance criteria
|
||
1. Admin can lower BSC's threshold from 12 to 3 on the live dev stack without a redeploy and a new webhook fires the safety gate with the new value within 30s.
|
||
2. Awaiting-confirmation table updates live as new blocks arrive (poll every 12s on BSC).
|
||
3. Audit log records every threshold change with admin user, before/after, timestamp.
|
||
|
||
---
|
||
|
||
## 4. Optional AML screening (seller-paid) — Task #10
|
||
|
||
### Problem
|
||
The Transaction Safety Provider has an `evaluateAmlPlaceholder()` stub that currently returns `status: skipped`. We want a real AML pass that the seller can *opt into* per-offer, with the seller covering the per-check API cost.
|
||
|
||
### Goal
|
||
1. Pick a provider (Chainalysis Address Screening, TRM Labs, Elliptic — open question). Default recommendation: Chainalysis Address Screening (cheapest, simplest API).
|
||
2. Per-offer setting: `requireAmlCheck: true|false` plus `amlCheckPricePaidBy: 'seller'`.
|
||
3. When AML required, the in-house checkout webhook hits the provider with the buyer's source address. Result feeds into Transaction Safety Provider as a real `aml_screening` check.
|
||
4. Seller's account is debited for the per-check cost from their Amanat balance (or the payment is partially withheld for the cost).
|
||
|
||
### Hard-known facts
|
||
- AML providers charge per-check (USD 0.10–0.50 typical). Some charge a flat monthly minimum.
|
||
- Chainalysis returns categories like `sanctions`, `darknet_marketplace`, `mixer`. Provider doesn't return PII.
|
||
- Most providers have rate limits (~50 rps).
|
||
|
||
### Open questions
|
||
1. **Provider choice.** Need a 1-page comparison: per-check cost, response latency, supported chains, sanctions-coverage scope.
|
||
2. **Failure mode.** If the AML API is down, do we pass-through (let the payment complete) or fail-closed (block)? Recommended: fail-closed only when seller explicitly opted in *and* enabled "block-on-provider-failure". Otherwise warn + log.
|
||
3. **Cost accounting.** Per-check cost is small; do we round up to nearest cent, batch by day, or pass-through exact?
|
||
|
||
### Scope
|
||
1. Add `requireAmlCheck` + `amlBlockOnFailure` fields to the `Offer` schema (or `SellerOffer`).
|
||
2. New `backend/src/services/payment/safety/amlProvider.ts` interface + `chainalysisProvider.ts` impl. Behind env flag `TRANSACTION_SAFETY_AML_PROVIDER=chainalysis`.
|
||
3. Transaction Safety Provider's `aml_screening` check now real, with `metadata.amlResult` persisted on the Payment record for audit.
|
||
4. Cost accounting: deduct per-check cost from seller's escrow on payment completion; surface this as a line item on the payment-details view.
|
||
5. Frontend offer-edit UI: a toggle "Require AML on incoming payments (cost: $X per payment, paid by you)".
|
||
6. Frontend admin UI for global AML provider configuration (provider, API key, per-chain enabled/disabled).
|
||
|
||
### Non-goals
|
||
- Buyer-side AML (we screen the buyer's *source* address, not the seller's identity).
|
||
- Custom AML rules / scoring beyond the provider's verdict.
|
||
|
||
### Acceptance criteria
|
||
1. A seller can opt into AML on a specific offer. Toggle persists.
|
||
2. An incoming payment to that offer triggers a real Chainalysis API call; the result is stored on the Payment record.
|
||
3. Verdict `sanctions` blocks the escrow gate; verdict `clean` lets it through.
|
||
4. The seller's settled amount is reduced by the AML check cost; a corresponding ledger entry is created.
|
||
5. Admin can rotate the Chainalysis API key without redeploy.
|
||
6. If the provider is down and `amlBlockOnFailure=true`, the payment stays in pending; a `provider_unavailable` reason surfaces in the admin dashboard.
|
||
|
||
---
|
||
|
||
## 5. Trezor support for admin signing — Task #11
|
||
|
||
### Problem
|
||
Today, admin actions that require signing (escrow release, refund, sweep of derived destinations once Task #7 ships) run from a hot-key in the backend env (`ADMIN_PRIVATE_KEY` or similar). That key is a single-point-of-compromise for all custodial funds.
|
||
|
||
### Goal
|
||
Replace the hot-key flow with a Trezor-mediated browser flow:
|
||
1. Admin connects a Trezor via WebUSB in the admin dashboard.
|
||
2. Admin approves an action in the UI; the unsigned transaction is built backend-side, sent to the browser, signed by the Trezor, broadcast from the browser.
|
||
3. The backend never has the private key. The Trezor seed never touches a network.
|
||
|
||
### Hard-known facts
|
||
- `@trezor/connect-web` is the maintained library for Trezor in browser. EIP-1193-compatible adapter exists.
|
||
- Trezor supports EVM signing for any chain; chain ID is part of the tx.
|
||
- WebUSB is Chromium-only. Firefox users need the Trezor Bridge native helper.
|
||
|
||
### Open questions
|
||
1. **Multi-admin.** If two admins both have a Trezor configured, do they both need to sign (m-of-n), or any one of them? Default: any one of them; m-of-n is out of scope here.
|
||
2. **Trezor model.** Trezor One vs Model T have different signing UX. We target both; the lib abstracts it.
|
||
3. **Fallback.** What if Trezor is unavailable when an urgent release is needed? Default: a "break-glass" hot-key path that admin can flip on for a 1-hour window, alarms blast Telegram.
|
||
|
||
### Scope
|
||
1. New module `frontend/src/web3/trezor/trezorConnector.ts` wrapping `@trezor/connect-web`.
|
||
2. Admin actions (release/refund/sweep) get a "Sign with Trezor" button that:
|
||
- Hits backend `POST /api/admin/actions/build-tx` returning unsigned tx bytes.
|
||
- Sends to Trezor for signing.
|
||
- Submits signed tx via wagmi `sendTransaction`.
|
||
- Calls `POST /api/admin/actions/confirm-tx` with the tx hash.
|
||
3. Backend supports both `confirmReleaseTx` flow (existing) and the new build-tx pattern.
|
||
4. Admin settings page to "register Trezor": stores the Trezor's address(es) so backend can reject signatures from unauthorized devices.
|
||
5. Audit log on every Trezor-signed action.
|
||
|
||
### Non-goals
|
||
- Multi-sig contracts (Safe etc.) — separate decision.
|
||
- Buyer-side Trezor (buyer already uses their own wallet via wagmi `injected()`).
|
||
- Mobile Trezor flow (desktop only for v1).
|
||
|
||
### Acceptance criteria
|
||
1. Admin can register a Trezor address; subsequent admin actions show "sign with Trezor" CTA.
|
||
2. End-to-end release of escrow: build → Trezor approves → tx broadcast → backend confirms.
|
||
3. If Trezor is unregistered or admin tries to sign with a different device, the backend rejects the confirm step.
|
||
4. Audit log entries include admin user, Trezor address, tx hash, action, before/after escrow state.
|
||
5. Break-glass hot-key path requires an explicit admin toggle, expires after 1h, fires a Telegram alarm.
|
||
|
||
---
|
||
|
||
## Shared dependencies / order-of-operations
|
||
|
||
- Task #8 (multichain) and Task #9 (confirmations) are independent and can land in parallel.
|
||
- Task #7 (ephemeral wallets) depends on Task #11 (Trezor) only for the sweep step — the address-generation half can ship first, sweep can land later with hot-key, then migrate to Trezor when #11 is done.
|
||
- Task #10 (AML) depends on nothing from the other four. It plugs into the existing Transaction Safety Provider.
|
||
- Task #11 (Trezor) is self-contained.
|
||
|
||
Tasks were sized for one experienced contributor to take any single one end-to-end without needing the others to land first. The integration glue (UI placement, navigation, telemetry) is left for the maintainer.
|