Skip to main content

hw_wallet/
error.rs

1use ble_transport::BleError;
2use thiserror::Error;
3use trezor_connect::thp::{BackendError, ThpWorkflowError};
4
5/// Failure classes an app can map to distinct screens without matching message text.
6#[derive(Debug, Clone, Copy, Eq, PartialEq)]
7pub enum WalletErrorKind {
8    Ble,
9    Workflow,
10    Device,
11    Validation,
12    Timeout,
13    /// The user rejected the request on the device, or cancelled PIN entry.
14    ActionCancelled,
15    /// The device is locked and expects PIN entry.
16    DeviceLocked,
17    /// The device is busy with another action.
18    DeviceBusy,
19    /// The device must confirm this host's connection before continuing.
20    ConnectionConfirmationRequired,
21    /// The device no longer knows this host; pair again.
22    PairingRequired,
23    /// The BLE link dropped during the operation.
24    Disconnected,
25    /// The firmware does not support the request; it may be too old.
26    Unsupported,
27}
28
29#[derive(Debug, Error)]
30pub enum WalletError {
31    #[error("invalid BIP32 path: {0}")]
32    InvalidBip32Path(String),
33    #[error(
34        "BLE peer removed pairing information: remove device from OS Bluetooth settings and pair again"
35    )]
36    PeerRemovedPairingInfo,
37    #[error("BLE error: {0}")]
38    Ble(#[from] BleError),
39    #[error("workflow error: {0}")]
40    Workflow(#[from] ThpWorkflowError),
41    #[error("signing error: {0}")]
42    Signing(String),
43    #[error(
44        "solana serialized tx is too short ({len} bytes, minimum {min}); provide full serialized transaction bytes"
45    )]
46    SolanaTxTooShort { len: usize, min: usize },
47    #[error("Solana transaction version {0} is not supported by firmware")]
48    UnsupportedSolanaTxVersion(u8),
49    /// A session could not be created; `hint` tells the user how to recover.
50    #[error("{hint}")]
51    SessionUnavailable {
52        hint: &'static str,
53        #[source]
54        source: ThpWorkflowError,
55    },
56}
57
58pub type WalletResult<T> = std::result::Result<T, WalletError>;
59
60impl WalletError {
61    pub fn kind(&self) -> WalletErrorKind {
62        match self {
63            Self::InvalidBip32Path(_)
64            | Self::Signing(_)
65            | Self::SolanaTxTooShort { .. }
66            | Self::UnsupportedSolanaTxVersion(_) => WalletErrorKind::Validation,
67            Self::PeerRemovedPairingInfo => WalletErrorKind::PairingRequired,
68            Self::Ble(error) => error.into(),
69            Self::Workflow(error) | Self::SessionUnavailable { source: error, .. } => error.into(),
70        }
71    }
72}
73
74impl From<&BleError> for WalletErrorKind {
75    fn from(error: &BleError) -> Self {
76        match error {
77            BleError::Timeout(_) => Self::Timeout,
78            BleError::Disconnected | BleError::NotificationStreamClosed => Self::Disconnected,
79            BleError::Btleplug(_)
80            | BleError::AdapterUnavailable
81            | BleError::MissingCharacteristic { .. } => Self::Ble,
82        }
83    }
84}
85
86impl From<&ThpWorkflowError> for WalletErrorKind {
87    fn from(error: &ThpWorkflowError) -> Self {
88        match error {
89            ThpWorkflowError::Backend(error) => error.into(),
90            ThpWorkflowError::PairingInteractionRequired => Self::PairingRequired,
91            ThpWorkflowError::InvalidPhase
92            | ThpWorkflowError::MissingHandshake
93            | ThpWorkflowError::MissingHandshakeCredentials
94            | ThpWorkflowError::AlreadyPaired
95            | ThpWorkflowError::NoCommonPairingMethod
96            | ThpWorkflowError::PairingAborted
97            | ThpWorkflowError::PairingController(_)
98            | ThpWorkflowError::UnexpectedPairingResponse
99            | ThpWorkflowError::PairedWithoutCredential
100            | ThpWorkflowError::Storage(_) => Self::Workflow,
101        }
102    }
103}
104
105impl From<&BackendError> for WalletErrorKind {
106    fn from(error: &BackendError) -> Self {
107        match error {
108            BackendError::TransportTimeout => Self::Timeout,
109            BackendError::Disconnected => Self::Disconnected,
110            BackendError::ActionCancelled => Self::ActionCancelled,
111            BackendError::DeviceLocked | BackendError::PinExpected => Self::DeviceLocked,
112            BackendError::DeviceBusy | BackendError::TransportBusy => Self::DeviceBusy,
113            BackendError::SessionConfirmationRequired => Self::ConnectionConfirmationRequired,
114            BackendError::UnsupportedRequest => Self::Unsupported,
115            BackendError::Device(_)
116            | BackendError::DeviceFirmwareError
117            | BackendError::DeviceError { .. } => Self::Device,
118            BackendError::UnsupportedPairingMethod => Self::Validation,
119            BackendError::Transport(_) => Self::Workflow,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::time::Duration;
128
129    #[test]
130    fn ble_errors_are_classified_by_variant() {
131        let cases = [
132            (
133                BleError::Timeout(Duration::from_secs(1)),
134                WalletErrorKind::Timeout,
135            ),
136            (BleError::Disconnected, WalletErrorKind::Disconnected),
137            (
138                BleError::NotificationStreamClosed,
139                WalletErrorKind::Disconnected,
140            ),
141            (BleError::AdapterUnavailable, WalletErrorKind::Ble),
142        ];
143        for (error, kind) in cases {
144            assert_eq!(WalletError::Ble(error).kind(), kind);
145        }
146    }
147
148    #[test]
149    fn device_failures_are_classified_for_distinct_app_screens() {
150        let cases = [
151            (
152                BackendError::ActionCancelled,
153                WalletErrorKind::ActionCancelled,
154            ),
155            (BackendError::PinExpected, WalletErrorKind::DeviceLocked),
156            (BackendError::DeviceLocked, WalletErrorKind::DeviceLocked),
157            (BackendError::DeviceBusy, WalletErrorKind::DeviceBusy),
158            (BackendError::TransportBusy, WalletErrorKind::DeviceBusy),
159            (
160                BackendError::SessionConfirmationRequired,
161                WalletErrorKind::ConnectionConfirmationRequired,
162            ),
163            (BackendError::Disconnected, WalletErrorKind::Disconnected),
164            (BackendError::TransportTimeout, WalletErrorKind::Timeout),
165            (
166                BackendError::UnsupportedRequest,
167                WalletErrorKind::Unsupported,
168            ),
169        ];
170        for (error, kind) in cases {
171            assert_eq!(
172                WalletError::from(ThpWorkflowError::Backend(error)).kind(),
173                kind
174            );
175        }
176        assert_eq!(
177            WalletError::from(ThpWorkflowError::PairingInteractionRequired).kind(),
178            WalletErrorKind::PairingRequired
179        );
180        assert_eq!(
181            WalletError::PeerRemovedPairingInfo.kind(),
182            WalletErrorKind::PairingRequired
183        );
184    }
185}