Skip to main content

trezor_connect_rs/
error.rs

1//! Error types for the Trezor Connect library.
2//!
3//! This module defines all error types that can occur during Trezor communication.
4
5use thiserror::Error;
6
7/// Result type alias for Trezor operations.
8pub type Result<T> = std::result::Result<T, TrezorError>;
9
10/// Main error type for Trezor operations.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum TrezorError {
14    /// Transport layer error (USB/Bluetooth communication)
15    #[error("Transport error: {0}")]
16    Transport(#[from] TransportError),
17
18    /// Protocol layer error (encoding/decoding)
19    #[error("Protocol error: {0}")]
20    Protocol(#[from] ProtocolError),
21
22    /// Device returned an error
23    #[error("Device error: {0}")]
24    Device(#[from] DeviceError),
25
26    /// THP (Trezor Host Protocol) error
27    #[error("THP error: {0}")]
28    Thp(#[from] ThpError),
29
30    /// Session management error
31    #[error("Session error: {0}")]
32    Session(#[from] SessionError),
33
34    /// Bitcoin-specific error
35    #[error("Bitcoin error: {0}")]
36    Bitcoin(#[from] BitcoinError),
37
38    /// Operation was cancelled
39    #[error("Operation cancelled")]
40    Cancelled,
41
42    /// Operation timed out
43    #[error("Operation timed out")]
44    Timeout,
45
46    /// I/O error (file operations)
47    #[error("I/O error: {0}")]
48    IoError(String),
49}
50
51/// Transport layer errors.
52#[derive(Debug, Error)]
53#[non_exhaustive]
54pub enum TransportError {
55    /// No Trezor device found
56    #[error("No Trezor device found")]
57    DeviceNotFound,
58
59    /// Device disconnected during operation
60    #[error("Device disconnected during operation")]
61    DeviceDisconnected,
62
63    /// Unable to open device
64    #[error("Unable to open device: {0}")]
65    UnableToOpen(String),
66
67    /// Unable to close device
68    #[error("Unable to close device: {0}")]
69    UnableToClose(String),
70
71    /// Data transfer error
72    #[error("Data transfer error: {0}")]
73    DataTransfer(String),
74
75    /// USB-specific error
76    #[cfg(feature = "usb")]
77    #[error("USB error: {0}")]
78    Usb(String),
79
80    /// Bluetooth-specific error
81    #[cfg(feature = "bluetooth")]
82    #[error("Bluetooth error: {0}")]
83    Bluetooth(String),
84
85    /// Permission denied
86    #[error("Permission denied: {0}")]
87    PermissionDenied(String),
88
89    /// Device is busy (already in use)
90    #[error("Device is busy")]
91    DeviceBusy,
92}
93
94/// Protocol layer errors.
95#[derive(Debug, Error)]
96#[non_exhaustive]
97pub enum ProtocolError {
98    /// Malformed message
99    #[error("Malformed message: {0}")]
100    Malformed(String),
101
102    /// Invalid message type
103    #[error("Invalid message type: {0}")]
104    InvalidMessageType(u16),
105
106    /// Message too short
107    #[error("Message too short: expected {expected}, got {actual}")]
108    MessageTooShort { expected: usize, actual: usize },
109
110    /// Invalid header
111    #[error("Invalid header")]
112    InvalidHeader,
113
114    /// Chunk header mismatch
115    #[error("Chunk header mismatch")]
116    ChunkHeaderMismatch,
117
118    /// Protobuf encoding error
119    #[error("Protobuf encode error: {0}")]
120    ProtobufEncode(String),
121
122    /// Protobuf decoding error
123    #[error("Protobuf decode error: {0}")]
124    ProtobufDecode(String),
125
126    /// Unexpected response type
127    #[error("Unexpected response type: expected {expected}, got {actual}")]
128    UnexpectedResponse { expected: String, actual: String },
129}
130
131/// Device errors returned by the Trezor.
132#[derive(Debug, Error)]
133#[non_exhaustive]
134pub enum DeviceError {
135    /// Device not connected or session not acquired
136    #[error("Device not connected or session not acquired")]
137    NotConnected,
138
139    /// Action cancelled on device
140    #[error("Action cancelled by user")]
141    ActionCancelled,
142
143    /// PIN is required
144    #[error("PIN is required")]
145    PinRequired,
146
147    /// Invalid PIN entered
148    #[error("Invalid PIN")]
149    InvalidPin,
150
151    /// PIN entry cancelled
152    #[error("PIN entry cancelled")]
153    PinCancelled,
154
155    /// Passphrase is required
156    #[error("Passphrase is required")]
157    PassphraseRequired,
158
159    /// Passphrase entry cancelled
160    #[error("Passphrase entry cancelled")]
161    PassphraseCancelled,
162
163    /// The device's derived session state did not match the expected state,
164    /// i.e. a different passphrase was entered than the one that created the
165    /// remembered wallet. Mirrors trezor-suite's `Device_InvalidState`.
166    #[error("Passphrase is incorrect (device state mismatch)")]
167    InvalidState,
168
169    /// Device is not initialized
170    #[error("Device is not initialized")]
171    NotInitialized,
172
173    /// Device needs firmware update
174    #[error("Device needs firmware update")]
175    FirmwareUpdateRequired,
176
177    /// Seed is not backed up
178    #[error("Seed is not backed up")]
179    SeedNotBackedUp,
180
181    /// Feature not supported
182    #[error("Feature not supported: {0}")]
183    NotSupported(String),
184
185    /// Device returned failure
186    #[error("Device failure: {code:?} - {message}")]
187    Failure { code: Option<i32>, message: String },
188
189    /// Device returned error response
190    #[error("Device error: code {code} - {message}")]
191    DeviceError { code: i32, message: String },
192
193    /// Button request (informational)
194    #[error("Button request: {0}")]
195    ButtonRequest(String),
196
197    /// Protobuf decode error
198    #[error("Protobuf decode error: {0}")]
199    ProtobufDecode(String),
200
201    /// Invalid input
202    #[error("Invalid input: {0}")]
203    InvalidInput(String),
204}
205
206impl DeviceError {
207    /// Map a Trezor protocol `Failure` (a `FailureType` code plus message) into
208    /// a typed `DeviceError`. Known PIN/action codes become their typed
209    /// variants so callers (and downstream FFI consumers) can react to a wrong
210    /// PIN, a cancelled PIN entry, etc.; unknown codes fall back to the generic
211    /// `DeviceError` so existing behavior is preserved.
212    ///
213    /// Codes match `FailureType` in `proto/messages-common.proto`.
214    pub fn from_failure(code: Option<i32>, message: String) -> Self {
215        match code {
216            Some(7) => DeviceError::InvalidPin,      // Failure_PinInvalid
217            Some(6) => DeviceError::PinCancelled,    // Failure_PinCancelled
218            Some(5) => DeviceError::PinRequired,     // Failure_PinExpected
219            Some(4) => DeviceError::ActionCancelled, // Failure_ActionCancelled
220            other => DeviceError::DeviceError {
221                code: other.unwrap_or(0),
222                message,
223            },
224        }
225    }
226}
227
228/// THP (Trezor Host Protocol) specific errors.
229#[derive(Debug, Error)]
230#[non_exhaustive]
231pub enum ThpError {
232    /// Channel allocation failed
233    #[error("Channel allocation failed")]
234    ChannelAllocationFailed,
235
236    /// Handshake failed
237    #[error("Handshake failed: {0}")]
238    HandshakeFailed(String),
239
240    /// Pairing required
241    #[error("Pairing required")]
242    PairingRequired,
243
244    /// Pairing failed
245    #[error("Pairing failed: {0}")]
246    PairingFailed(String),
247
248    /// Invalid credentials
249    #[error("Invalid credentials")]
250    InvalidCredentials,
251
252    /// Encryption error
253    #[error("Encryption error: {0}")]
254    EncryptionError(String),
255
256    /// Decryption error
257    #[error("Decryption error: {0}")]
258    DecryptionError(String),
259
260    /// ACK not received
261    #[error("ACK not received")]
262    AckNotReceived,
263
264    /// Invalid sync bit
265    #[error("Invalid sync bit")]
266    InvalidSyncBit,
267
268    /// State missing
269    #[error("THP state missing")]
270    StateMissing,
271
272    /// Session creation error
273    #[error("Session error: {0}")]
274    SessionError(String),
275}
276
277/// Session management errors.
278#[derive(Debug, Error)]
279#[non_exhaustive]
280pub enum SessionError {
281    /// Session not found
282    #[error("Session not found")]
283    NotFound,
284
285    /// Wrong previous session
286    #[error("Wrong previous session")]
287    WrongPrevious,
288
289    /// Session already acquired
290    #[error("Session already acquired by another client")]
291    AlreadyAcquired,
292
293    /// Session expired
294    #[error("Session expired")]
295    Expired,
296}
297
298/// Bitcoin-specific errors.
299#[derive(Debug, Error)]
300#[non_exhaustive]
301pub enum BitcoinError {
302    /// Invalid derivation path
303    #[error("Invalid derivation path: {0}")]
304    InvalidPath(String),
305
306    /// Invalid address
307    #[error("Invalid address: {0}")]
308    InvalidAddress(String),
309
310    /// Invalid transaction
311    #[error("Invalid transaction: {0}")]
312    InvalidTransaction(String),
313
314    /// Insufficient funds
315    #[error("Insufficient funds")]
316    InsufficientFunds,
317
318    /// Invalid signature
319    #[error("Invalid signature")]
320    InvalidSignature,
321
322    /// Network mismatch
323    #[error("Network mismatch: expected {expected}, got {actual}")]
324    NetworkMismatch { expected: String, actual: String },
325}
326
327// Implement From for common error types
328
329impl From<std::io::Error> for TrezorError {
330    fn from(err: std::io::Error) -> Self {
331        TrezorError::Transport(TransportError::DataTransfer(err.to_string()))
332    }
333}
334
335impl From<prost::DecodeError> for ProtocolError {
336    fn from(err: prost::DecodeError) -> Self {
337        ProtocolError::ProtobufDecode(err.to_string())
338    }
339}
340
341impl From<prost::EncodeError> for ProtocolError {
342    fn from(err: prost::EncodeError) -> Self {
343        ProtocolError::ProtobufEncode(err.to_string())
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn from_failure_maps_pin_invalid() {
353        // Failure_PinInvalid = 7 -> wrong PIN entered
354        assert!(matches!(
355            DeviceError::from_failure(Some(7), "invalid pin".to_string()),
356            DeviceError::InvalidPin
357        ));
358    }
359
360    #[test]
361    fn from_failure_maps_pin_cancelled() {
362        // Failure_PinCancelled = 6
363        assert!(matches!(
364            DeviceError::from_failure(Some(6), "cancelled".to_string()),
365            DeviceError::PinCancelled
366        ));
367    }
368
369    #[test]
370    fn from_failure_maps_pin_expected() {
371        // Failure_PinExpected = 5 -> PIN required
372        assert!(matches!(
373            DeviceError::from_failure(Some(5), "pin expected".to_string()),
374            DeviceError::PinRequired
375        ));
376    }
377
378    #[test]
379    fn from_failure_maps_action_cancelled() {
380        // Failure_ActionCancelled = 4
381        assert!(matches!(
382            DeviceError::from_failure(Some(4), "action cancelled".to_string()),
383            DeviceError::ActionCancelled
384        ));
385    }
386
387    #[test]
388    fn from_failure_unknown_code_stays_generic() {
389        // Unknown code (e.g. Failure_FirmwareError = 99) falls back to generic,
390        // preserving both the code and the message.
391        match DeviceError::from_failure(Some(99), "boom".to_string()) {
392            DeviceError::DeviceError { code, message } => {
393                assert_eq!(code, 99);
394                assert_eq!(message, "boom");
395            }
396            other => panic!("expected generic DeviceError, got {other:?}"),
397        }
398    }
399
400    #[test]
401    fn from_failure_missing_code_stays_generic() {
402        match DeviceError::from_failure(None, "no code".to_string()) {
403            DeviceError::DeviceError { code, message } => {
404                assert_eq!(code, 0);
405                assert_eq!(message, "no code");
406            }
407            other => panic!("expected generic DeviceError, got {other:?}"),
408        }
409    }
410}