Problem
Native apps still classify some Trezor failures outside the typed TrezorException hierarchy:
-
Firmware faults (protocol code 99) — Android matches message substrings such as "Device error (code 99)" and "Firmware error" in TrezorExceptionExt.kt. This is fragile and duplicates logic core already owns in src/modules/trezor/errors.rs, where DeviceError::DeviceError { code, message } is currently mapped to generic TrezorError::DeviceError.
-
Locked device — Android detects pinProtection == true && unlocked == false from TrezorFeatures in TrezorRepo.requireUnlocked() and synthesizes TrezorException.DeviceBusy(). That conflates two distinct UX states:
- Device locked → unlock guidance
- Device busy → wait / reconnect guidance
iOS has similar string-based handling in TrezorManager.swift.
Consumer-side heuristics are easy to break when error copy changes and force both apps to maintain parallel classification logic.
Proposed change
Extend the exported TrezorError / TrezorException enum in src/modules/trezor/errors.rs:
FirmwareError
Map Trezor protocol failure code 99 (Failure_FirmwareError) to a dedicated variant instead of generic DeviceError.
- Preserve
error_details for diagnostics.
- Update
test_failure_code_unknown_stays_generic_device_error in src/modules/trezor/tests.rs — that test currently asserts code 99 stays generic; it should assert FirmwareError instead.
DeviceLocked
Add a distinct variant for “device has PIN protection and is currently locked”.
Options (pick one in implementation):
- Map from an existing trezor-connect-rs signal if available at connect/features refresh time.
- Or expose locked state explicitly when
get_features() reports pin_protection && !unlocked before an operation proceeds, returning DeviceLocked instead of requiring apps to reimplement the rule.
Keep DeviceBusy for transport/session contention only. Do not overload it for locked state.
Regenerate UniFFI bindings after the enum change.
Tests
- Code 99 device failure maps to
TrezorError::FirmwareError (not generic DeviceError).
- Unrelated device error codes still map to
DeviceError.
- Locked-device path returns
DeviceLocked and is distinct from DeviceBusy.
- Existing
DeviceBusy transport mapping tests remain unchanged.
Acceptance criteria
- FFI exposes
TrezorException.FirmwareError and TrezorException.DeviceLocked.
- No message-string sniffing required in native apps for these two cases.
- Rust tests, Clippy, and formatting pass.
- Bindings published in a core release.
Consumer follow-up
After release, update bitkit-android and bitkit-ios:
- Remove
isTrezorFirmwareError() message heuristics; match TrezorException.FirmwareError.
- Remove
requireUnlocked() → DeviceBusy synthesis; match TrezorException.DeviceLocked.
- Keep UX mapping:
DeviceLocked → unlock info toast
FirmwareError → reconnect-hardware toast
DeviceBusy → busy / backoff guidance
Related
Priority
Medium — not blocking v1 Trezor ship; apps have working heuristics. Reduces long-term fragility as more Trezor flows are added.
Problem
Native apps still classify some Trezor failures outside the typed
TrezorExceptionhierarchy:Firmware faults (protocol code 99) — Android matches message substrings such as
"Device error (code 99)"and"Firmware error"inTrezorExceptionExt.kt. This is fragile and duplicates logic core already owns insrc/modules/trezor/errors.rs, whereDeviceError::DeviceError { code, message }is currently mapped to genericTrezorError::DeviceError.Locked device — Android detects
pinProtection == true && unlocked == falsefromTrezorFeaturesinTrezorRepo.requireUnlocked()and synthesizesTrezorException.DeviceBusy(). That conflates two distinct UX states:iOS has similar string-based handling in
TrezorManager.swift.Consumer-side heuristics are easy to break when error copy changes and force both apps to maintain parallel classification logic.
Proposed change
Extend the exported
TrezorError/TrezorExceptionenum insrc/modules/trezor/errors.rs:FirmwareErrorMap Trezor protocol failure code 99 (
Failure_FirmwareError) to a dedicated variant instead of genericDeviceError.error_detailsfor diagnostics.test_failure_code_unknown_stays_generic_device_errorinsrc/modules/trezor/tests.rs— that test currently asserts code 99 stays generic; it should assertFirmwareErrorinstead.DeviceLockedAdd a distinct variant for “device has PIN protection and is currently locked”.
Options (pick one in implementation):
get_features()reportspin_protection && !unlockedbefore an operation proceeds, returningDeviceLockedinstead of requiring apps to reimplement the rule.Keep
DeviceBusyfor transport/session contention only. Do not overload it for locked state.Regenerate UniFFI bindings after the enum change.
Tests
TrezorError::FirmwareError(not genericDeviceError).DeviceError.DeviceLockedand is distinct fromDeviceBusy.DeviceBusytransport mapping tests remain unchanged.Acceptance criteria
TrezorException.FirmwareErrorandTrezorException.DeviceLocked.Consumer follow-up
After release, update
bitkit-androidandbitkit-ios:isTrezorFirmwareError()message heuristics; matchTrezorException.FirmwareError.requireUnlocked()→DeviceBusysynthesis; matchTrezorException.DeviceLocked.DeviceLocked→ unlock info toastFirmwareError→ reconnect-hardware toastDeviceBusy→ busy / backoff guidanceRelated
Priority
Medium — not blocking v1 Trezor ship; apps have working heuristics. Reduces long-term fragility as more Trezor flows are added.