Skip to content

CW-1624: Trezor native integration cleanup (connect, restore, send) - #3631

Open
sethforprivacy wants to merge 30 commits into
devfrom
claude/trezor-integration-cleanup-953a8e
Open

sethforprivacy wants to merge 30 commits into
devfrom
claude/trezor-integration-cleanup-953a8e

Conversation

@sethforprivacy

Copy link
Copy Markdown
Contributor

Follow-up to #3601 (this branch includes its commits and Vik's "reuse passphrase settings on reconnect" patch; the diff shrinks to the last commit once #3601 merges).

Fixes for the CW-1624 test findings on Trezor Safe 7 (Monero + Bitcoin):

Connect / pairing

  • One-off ThpUnallocatedChannel: the view model reused its in-memory THP state (phase=paired) across connects, so trezor_flutter skipped the handshake and used a channel the device had dropped. Every attempt now rebuilds state from the persisted pairing credentials; a failed attempt no longer wipes them.
  • Retry / exit go through the pending connectDevice call. Before, "Try again" fired a second connect whose success never reached the page (you were left on the device list), and exiting the sheet left connectDevice awaiting a completer forever with isConnecting stuck.
  • No second passphrase session when the device has "passphrase always on device" (createChannel() already bound one via the on-device prompt). Passphrase options are hidden in that mode; the settings sheet is skipped when nothing is left to configure. "Passphrase entry" state only shows when a session is actually created.
  • Already-connected BLE devices are listed on the connect page (a connected peripheral does not advertise) and the restore flow skips the page when the device is already connected. Scan results are de-duplicated.
  • Real copy for the auto-connect subtitle; Trezor errors are surfaced via interpretErrorCode instead of the Ledger connection message.

Restore

  • SelectHardwareWalletAccountPage / MoneroHardwareWalletOptionsPage popped themselves on any Trezor error and showed the alert on a disposed context (Bitcoin "dropped back to device list" with no message). The reason is shown first.
  • Monero hardware restores now pin the refresh height like the seed restore does ("1 day ago" landed ~15k blocks back). Also fixed the no-op date..subtract margin and the -1 height sentinel.

Send

  • Trezor commit threw a bare empty String from the wrong FFI object → blank "Transaction Error". It now reads Wallet_errorString and throws a typed exception; the banner has a fallback text.
  • "Max" on a 2nd+ recipient: the send page reactions were bound to output 0 because the selected index was not observable (not Trezor-specific).

Resync sheet: new copy + persisted "Do not show me this again" (shouldShowTrezorResyncInfo).

Strings added to all 31 locales via tool/append_translation.dart.

Not addressed here: the multi-recipient Monero Trezor signing failure itself (error is now visible; likely in trezor_flutter's range-proof batching / commitTrezor(0)), THP request timeout, Ledger-only signing progress UI.

Not yet tested on hardware.

🤖 Generated with Claude Code

konstantinullrich and others added 12 commits September 1, 2026 17:35
…stead-of-app

# Conflicts:
#	pubspec.lock
#	res/values/strings_ar.arb
#	res/values/strings_bg.arb
#	res/values/strings_cs.arb
#	res/values/strings_es.arb
#	res/values/strings_fa.arb
#	res/values/strings_fr.arb
#	res/values/strings_gn.arb
#	res/values/strings_ha.arb
#	res/values/strings_hi.arb
#	res/values/strings_hr.arb
#	res/values/strings_hy.arb
#	res/values/strings_id.arb
#	res/values/strings_it.arb
#	res/values/strings_ja.arb
#	res/values/strings_ko.arb
#	res/values/strings_my.arb
#	res/values/strings_nl.arb
#	res/values/strings_pl.arb
#	res/values/strings_pt.arb
#	res/values/strings_ru.arb
#	res/values/strings_th.arb
#	res/values/strings_tl.arb
#	res/values/strings_tr.arb
#	res/values/strings_uk.arb
#	res/values/strings_ur.arb
#	res/values/strings_vi.arb
#	res/values/strings_yo.arb
#	res/values/strings_zh.arb
…d of re-asking

Follow-up to #3601 (CW-1624). That PR correctly moves passphrase entry to
connect time and removes the per-operation session, but sending (and key-image
sync / swaps) still prompted for the passphrase inside the app: those flows
push ConnectDevicePage whenever the Trezor is not on a live BLE session, and
connectDevice() always ran the "Almost ready" settings sheet again. Nothing
persisted the choice, so every reconnect re-asked, and the sheet defaulted to
app-side entry.

What changes:

- HardwareWalletViewModel gains two no-op hooks: prepareReconnect(wallet) and
  rememberWalletSettings(wallet).
- ConnectDevicePageParams gains an optional reconnectWallet. The page calls
  prepareReconnect(reconnectWallet) right before connectDevice(). Restore and
  new-wallet flows leave it null, so they still show the settings sheet.
- TrezorConnectViewModel:
  * persists the TrezorDeviceSettings a session was created with, per wallet,
    in SecureStorage (mode "device" / "app" / "none" under
    com.cakewallet.trezor/passphrase_mode/<type>_<name>; app-side passphrase,
    when chosen, under com.cakewallet.trezor/passphrase/<type>_<name>);
  * on a reconnect for a known wallet, feeds those settings straight into
    createSession() and skips the in-app sheet. "device" mode re-creates the
    session with TrezorPassphrase.onDevice() (the device asks once, as it
    must), "app" mode with the stored value (silent), "none" creates no
    passphrase session;
  * saves the settings in initWallet() and rememberWalletSettings(), and
    clears them when a connect fails.
- The preset is consumed at the top of connectDevice() so a failed attempt can
  never leak it into an unrelated later connect.
- WalletHardwareRestoreViewModel.process() calls rememberWalletSettings() so a
  freshly restored wallet is covered before its first send.
- Every reconnect call site passes reconnectWallet: new-UI send page, key-image
  sync sheet, swap confirm sheet, exchange trade page, legacy send page.
- The settings sheet now defaults "Enter passphrase on device" to on when the
  passphrase toggle is enabled, matching the intent of #3601.

Net effect on a Trezor Safe 7 Monero wallet with a passphrase: after the first
setup, tapping Send no longer shows the in-app settings/passphrase sheet. If
the BLE session is still up nothing is asked at all; if it has to reconnect,
the device prompts once (device mode) or the stored passphrase is applied
silently (app mode).

Not covered here (separate issues): the missing timeout on THP requests that
turns a dropped reply into a permanent "Proceed on device", and the
Ledger-only signing-progress state for Monero.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…CW-1624)

Follows up PR #3601 and the "reuse passphrase settings on reconnect" patch.

Connect / pairing (TrezorConnectViewModel, ConnectDevicePage, pairing sheet):
- Start every connection attempt from a fresh THP state rebuilt from the
  persisted pairing credentials. The in-memory state kept phase=paired across
  connects, so trezor_flutter skipped the handshake and talked on a channel
  the device had dropped -> one-off ThpUnallocatedChannel. A failed attempt
  no longer wipes the credentials from memory either.
- Retry and exit now go through the pending connectDevice call
  (retryPairing / cancelPairing). Before, "Try again" fired a second
  connectDevice whose success never reached the page, and exiting the sheet
  left connectDevice awaiting a completer forever with isConnecting stuck.
- Do not create a second passphrase session when the device has
  "passphrase always on device": createChannel() already bound one via the
  on-device prompt. Hide the passphrase options in that case and skip the
  settings sheet entirely when nothing is left to configure.
- Only show the "Passphrase entry" state when a passphrase session is
  actually being created.
- List already-connected BLE devices on the connect page (a connected
  peripheral does not advertise) and skip the page in the restore flow when
  the device is already connected; de-duplicate scan results.
- Real copy for the auto-connect toggle subtitle; Trezor errors are surfaced
  via interpretErrorCode instead of the generic Ledger message.

Restore:
- SelectHardwareWalletAccountPage / MoneroHardwareWalletOptionsPage popped
  themselves on any Trezor error (all mapped to ledger_connection_error) and
  then showed the alert on a disposed context, dropping the user back to
  the device list with no message. Show the reason first.
- Pin the Monero refresh height on hardware (from-keys / from-device)
  restores like the seed restore does; restoring "1 day ago" landed ~15k
  blocks back. Fix the no-op `date..subtract` margin and the -1 sentinel.

Send:
- Trezor commit threw a bare empty String read from the wrong FFI object,
  producing an empty "Transaction Error" banner; read Wallet_errorString and
  throw a typed exception. Fallback text when an error is empty.
- "Max" on a 2nd+ recipient: the send page's per-output reactions were
  bound to output 0 because the selected index was not observable.

Resync sheet: new copy and a persisted "Do not show me this again" option
(shouldShowTrezorResyncInfo).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

changes in dependencies

changed git refs:
  universal_ble
    url   https://github.com/cake-tech/universal_ble.git -> https://github.com/cake-tech/universal_ble.git
    ref   6735c1bdf7f42c501d51c20564eb957c3a12b981 -> 9952fc72ded48891844d7786c0fa14c4cae1ce0b
    path  - -> -
     .../UniversalBlePeripheralPlugin.swift             | 77 +++++++++++++++-------
     example/macos/Podfile.lock                         |  2 +-
     2 files changed, 54 insertions(+), 25 deletions(-)

sethforprivacy and others added 4 commits September 16, 2026 11:24
Review follow-ups on #3631:

- Cancellation now has its own signal (_cancelCompleter) that every awaited
  stage of a connect attempt races against, including the BLE connect that
  runs before any PIN/settings completer exists. A link that comes up after
  the user exited is disconnected, and the attempt re-checks the flag between
  stages so it can never report success (or park on a new completer) after
  the sheet has closed.
- getAutoPairingCredentials(): cancellation is re-thrown instead of being
  swallowed by the "auto-pairing is optional" catch.
- Send page "Max": re-resolve the tapped recipient's index after the async
  balance lookup so removing/re-ordering recipients meanwhile cannot hit a
  stale controller index.
- Connect page: de-duplicate devices by transport id instead of name, so two
  devices sharing a name both stay selectable (HardwareWalletDevice.id).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…anup

The SDK disconnects by device id through a shared connection manager, so
the late disconnect of a link that came up after the user cancelled could
tear down the link of the next attempt on the same device. The late connect
and its disconnect are now tracked as a pending cleanup that the next
attempt awaits before connecting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nally; keep app passphrase under on-device mode; pop restore pages on Trezor link loss

Panel review follow-ups on #3631:

- proceed_on_device_sheet: if the sheet route is destroyed without its own
  buttons (e.g. the app locks and the unlock navigation clears the stack),
  dispose() cancels a still-pending connect so isConnecting cannot stay
  stuck for the rest of the session.
- Restore pages treated only the Ledger connection string as "link lost";
  since Trezor errors now have their own strings, they never went back to
  the connect page. isHardwareWalletConnectionError covers all of them.
- A wallet set up with an app-side passphrase keeps that record when the
  device is later switched to "passphrase always on device"; previously a
  reconnect rewrote the mode to "device" and deleted the stored secret.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… auto-submit pairing code, Trezor how-to-connect steps

- Restoring another wallet while the Trezor is already connected skipped
  straight to the accounts and silently reused the live session, so there
  was no way to pick a (different or no) passphrase. The scan is still
  skipped, but prepareNewWalletSession() now shows the settings sheet and
  binds a fresh session with the chosen passphrase (explicitly empty when
  none), or lets the device ask when it forces on-device entry.
- The pairing code is submitted as soon as the sixth digit is typed; the
  arrow stays as a fallback and a new code request resets the field.
- "How to connect" shows Trezor-specific steps (menu, Pair & Connect, Pair
  new device) instead of the Ledger ones. Strings added to all locales.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sethforprivacy and others added 8 commits September 16, 2026 16:46
universal_ble's peripheral plugin instantiated CBPeripheralManager in its
initializer, i.e. during plugin registration on every launch, so iOS asked
for Bluetooth permission before the user had touched anything Bluetooth
related. Pin universal_ble to the cake-tech fork commit that backports the
upstream fix (defer CBPeripheralManager creation until the peripheral API
is used). The prompt now appears only when the hardware wallet connect page
starts scanning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nishes syncing

A freshly restored Monero wallet on a Trezor is view-only on the phone, so
until the key images are imported from the device every output looks
unspent and the balance is too high. The sync used to be offered only when
the user tried to send. Offer it once per wallet load as soon as a sync
completes with unknown key images, e.g. right after a restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 24pt icon was centred on the text line box while the glyphs sit lower
in it, so the full-height Trezor icon floated above the name. Align the
icon to the text baseline and draw it at 20pt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the resync page a Material ancestor

- WalletInfoBar: lay the icon out inline (WidgetSpan, middle alignment) so
  the font metrics position it instead of the Row's line box; verified by
  rendering the widget with the app theme and fonts. Row-level baseline
  alignment did not propagate through the AnimatedSwitcher and left the
  icon top-aligned.
- SyncKeyImagesSheet is pushed as a bare route; without a Material ancestor
  its texts rendered in the red/yellow fallback style.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ssphrase records

Security review follow-ups on #3631:

- The live Trezor session is now attributed to a wallet (_sessionWalletKey).
  Already-connected spend/sync/buy flows call initWallet(), which used to
  attach whatever session was open and persist its settings under the
  current wallet. With one device and two Cake wallets that could sign or
  import key images with wallet A's passphrase while wallet B was open, and
  copy A's passphrase into B's stored record. initWallet() now rebinds the
  session for the wallet first (silently from its stored settings, or via
  the settings sheet) and refuses to persist settings for a wallet the
  session does not belong to.
- Per-wallet settings (mode + app-side passphrase) moved into
  TrezorWalletSettingsStorage and are deleted when the wallet is removed and
  re-keyed when it is renamed, so a passphrase no longer outlives its wallet
  or gets applied to a later wallet reusing the same name.
- _presetSettings is consumed before connectDevice's early returns so a
  rejected call cannot leave a preset for an unrelated later connect.
- TrezorDeviceSettings moved to core alongside the storage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing; rebind unattributed sessions

A Bitcoin passphrase wallet could reach signing with a session bound to
the wrong (empty) passphrase and fail with a script/pubkey mismatch: the
stored settings were wrong, or the live session had never been attributed
to a wallet (abandoned restore) and was taken as this wallet's.

- A session that is not attributed to the wallet being used, including an
  unattributed one, is now rebound first.
- After (re)binding, the device is asked for the account key (Bitcoin:
  account xpub at the wallet's derivation path; Monero: primary address,
  without showing it on the device) and compared with the wallet. On a
  mismatch the passphrase settings sheet is shown once and the check
  repeated; a persistent mismatch aborts with a clear error instead of a
  wrong-wallet signature.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es each time, and show step-specific device instructions

- The per-wallet record now stores only how the passphrase is supplied
  (none / on device / in app); the app-side passphrase itself is never
  persisted any more (earlier records are scrubbed). Reconnecting an
  app-passphrase wallet shows a passphrase field; a device-passphrase
  wallet shows "enter it on your Trezor"; the full settings sheet only
  appears when nothing is known or the keys do not match.
- Every hardware-wallet connect callback now awaits initWallet() before
  continuing. It performs device round-trips (session rebind and key
  verification) since the last change, and callers that fired it without
  awaiting proceeded to sign with no hardware service attached
  ("type 'Null' is not a subtype of type 'BitcoinHardwareWalletService'").
- Replace the catch-all "Proceed on your device" with what the Trezor
  actually asks for at each step: Bluetooth pairing-code match and pairing
  consent before the security code, auto-connect consent, passphrase on
  device, watch-only credential export (Monero restore), key image sync
  confirmation, and transaction review (each recipient, then fee/total).
  Strings added to all locales.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…reword the signing instruction

- The in-app passphrase screen now uses the same BaseTextFormField,
  reveal toggle, hint and illustration as the hot-wallet passphrase sheet.
- Signing instruction copy: "Review all transaction details on your
  Trezor match."

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@konstantinullrich
konstantinullrich changed the base branch from dev to CW-1624-trezor-passphrase-on-trezor-device-instead-of-app September 17, 2026 11:56
@konstantinullrich
konstantinullrich changed the base branch from CW-1624-trezor-passphrase-on-trezor-device-instead-of-app to dev September 17, 2026 11:57
…ed key-image sync recoverable

- Pin trezor_flutter/trezor_usb_transport to the cake-tech branch that
  ignores notifications on a disposed gateway and drops malformed
  fragments instead of failing with "Bad state: Not enough bytes to read"
  (seen by QA when the device timed out and auto-reconnected during the
  key-image sync flow).
- Key-image sync failure no longer leaves the resync sheet stuck on the
  syncing state: it returns to the explanation, shows the reason, and
  resets a dead client so the next attempt reconnects cleanly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sethforprivacy and others added 5 commits September 17, 2026 20:35
dev now carries the squash of #3601 (the branch this work was built on),
so every conflict was between our follow-up changes and content that is
already an ancestor here. Resolved by keeping this branch's side:

- trezor_connect_view_model.dart / proceed_on_device_sheet.dart: dev's copies
  are byte-identical to the #3601 tip in our history.
- res/values/*.arb: keep the added `auto_connect_desc` next to `auto_connect`.
- pubspec_base.yaml / pubspec_overrides.yaml / pubspec.lock: keep the
  trezor_flutter ref 73e7045 (descends from dev's 0277176).
- cw_monero wallet_manager.dart: keep the guarded setRefreshFromBlockHeight
  and its comment; cw_core get_height_by_date.dart: same fix, dev's `const`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e conversations

QA: after several Monero sends, a block landed while a transaction was
being signed on the Trezor. The wallet flipped to "synced" with an
unknown-key-image change output, so the automatic key-image sync offer was
pushed on top of the send flow. Continuing it started a second THP
conversation while the signing one was still on the device, and the Trezor
stopped responding until it was power-cycled and re-paired.

- MoneroTrezorService now owns a static device mutex; signing, key-image
  sync, the watch-only export and the session address check all run under
  it, so two device conversations can no longer interleave. Exposed as
  `Monero.isTrezorBusy()`.
- The sync-status reaction only offers the key-image sync when the user is
  on the dashboard and the device is idle. Otherwise it waits (MobX `when`
  on the current route) and offers once the user is back on the dashboard,
  instead of consuming the once-per-wallet offer or interrupting a flow.
  The pending offer is dropped when the active wallet changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ardown, recover the key-image sheet; restore dev hunks dropped in the merge

Review round 3 (Astra) on d712076.

1. A pairing sheet that had already succeeded could cancel the corrective
   rebind that _ensureSessionFor starts right after it: its dispose() runs
   after the pop animation and read the view model's *current* isConnecting
   / paringState. The sheet now captures `pairingAttempt` when it opens,
   records its own success, and `cancelPairing(attempt:)` ignores a sheet
   that is not the current attempt.

2. Cancelling a session rebind at the in-app passphrase stage makes the SDK
   tear the BLE link down asynchronously, but that teardown was never
   registered in _pendingCleanup (rebind had a no-op onFailure), so the next
   connect could bring up a link while the old one was still going down.
   cancelPairing now chains the SDK cancel into _pendingCleanup, and a rebind
   whose link died resets the client.

3. The key-image sheet awaited initWallet without handling the new
   TrezorSessionMismatchException, leaving it on "Proceed on device" with no
   Continue button. Session bind + sync is now one recoverable step: back to
   the initial state on failure or back-out; the view model shows the reason
   (unless the user cancelled) so every initWallet caller gets the same
   message without local handling. TrezorSessionMismatchException carries
   `cancelled`.

4. Merge ee4c21b used `git checkout --ours` on conflicted files, which also
   discarded dev's auto-merged hunks in them. Restored: the guarded
   setRefreshFromBlockHeight in restoreWalletFromSpendKeySync, the
   cake_backup pin reverted by #3634 (3aba867), and the unused import removal
   in get_height_by_date.dart. Those files now differ from dev only by this
   branch's intended changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ound instead of leaking the exception

Review round 3 follow-up. Send, Swap, the buy providers and the dEuro
savings page awaited initWallet() with no handling for
TrezorSessionMismatchException, so a user simply exiting the passphrase
prompt for a wallet the device was not yet bound to bubbled up to the
application error handler and could show the generic report-an-error
dialog. The same flows also re-checked only "is the device still
connected?" after the connect page, which is not the same as "is the
session bound to this wallet".

- HardwareWalletViewModel.tryInitWallet(): typed result; false when the
  session could not be bound (the user has been told why unless they
  cancelled). Other failures still propagate.
- ensureHardwareWalletReady(context, vm, wallet): one place for the
  connect-page-then-bind dance that every flow duplicated; returns false on
  back-out or bind failure and never treats a connected-but-unbound device
  as ready. Push and pop use the same NavigatorState.
- All six flows use it and stop on false. The dEuro page's
  _requireHardwareWallet now returns that result and its actions honour it.
- TrezorSessionMismatchException moves to the base view model file so the
  base can consume it without a circular import.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dev's #3638 (thread-safety and BLE support for Ledger and Trezor) landed
the same ideas this branch already carried, so the merge reconciles two
implementations rather than picking sides:

- MoneroTrezorService: dev's `runBlocking` mutex naming is kept; this
  branch's `isBusy` flag is added on top because the key-image offer gate
  reads it. The session address check keeps using the same lock.
- HardwareWalletViewModel: dev's `getConnectedBleDevices` replaces this
  branch's `getConnectedDevices`; the connect page keeps this branch's
  loader (de-duplicates by device id) on dev's method name, and dev's
  duplicate loader is dropped.
- TrezorConnectViewModel: dev's BLE link-state listener is kept, registered
  in `_connect` where the interface is chosen (dev registered it in the old
  connectDevice body this branch had already restructured). The listener now
  also clears the session attribution when it drops the client, and its log
  line says Trezor rather than Ledger. dev's `close()` is kept as is.
- trezor_flutter: dev's pin 591d4b8 supersedes this branch's fork ref
  73e7045 (same gateway fix plus client safeguards). pubspec.lock refreshed
  with `flutter pub get`.

Every conflicted file was diffed against dev after resolution; the only
remaining differences are this branch's intended changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants