1use ble_transport::BleError;
2use thiserror::Error;
3use trezor_connect::thp::{BackendError, ThpWorkflowError};
4
5#[derive(Debug, Clone, Copy, Eq, PartialEq)]
7pub enum WalletErrorKind {
8 Ble,
9 Workflow,
10 Device,
11 Validation,
12 Timeout,
13 ActionCancelled,
15 DeviceLocked,
17 DeviceBusy,
19 ConnectionConfirmationRequired,
21 PairingRequired,
23 Disconnected,
25 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 #[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}