1use crate::error::ScopeError;
7use owo_colors::OwoColorize;
8use std::io::IsTerminal;
9
10fn is_tty_stderr() -> bool {
12 std::io::stderr().is_terminal()
13}
14
15pub fn display_error(e: &ScopeError) {
19 display_error_styled(e, is_tty_stderr())
20}
21
22fn display_error_styled(e: &ScopeError, tty: bool) {
24 let msg = match e {
25 ScopeError::NotFound(inner) => inner.clone(),
26 other => format!("{}", other),
27 };
28
29 if tty {
30 eprintln!("\n {} {}", "✗".red().bold(), msg.red());
31 } else {
32 eprintln!("\n ✗ {}", msg);
33 }
34
35 if let Some(hint) = error_suggestion(e) {
36 if tty {
37 eprintln!("\n {}", hint.dimmed());
38 } else {
39 eprintln!("\n {}", hint);
40 }
41 }
42 eprintln!();
43}
44
45pub fn error_suggestion(e: &ScopeError) -> Option<&'static str> {
47 match e {
48 ScopeError::InvalidAddress(_) => Some(
49 "Ensure the address format matches the target chain.\n \
50 EVM: 0x followed by 40 hex characters\n \
51 Solana: base58 encoded public key\n \
52 Tron: T followed by base58 characters",
53 ),
54 ScopeError::InvalidHash(_) => Some(
55 "Ensure the transaction hash matches the target chain.\n \
56 EVM: 0x followed by 64 hex characters\n \
57 Solana: base58 encoded signature",
58 ),
59 ScopeError::Config(_) => Some("Run `scope setup` to create or repair your configuration."),
60 ScopeError::Request(_) | ScopeError::Network(_) => Some(
61 "Check your network connection and try again.\n \
62 Use -v for more details on the failing request.",
63 ),
64 ScopeError::Api(msg)
65 if msg.contains("401") || msg.contains("403") || msg.contains("key") =>
66 {
67 Some(
68 "Your API key may be missing or invalid.\n Run `scope setup --key <provider>` to configure it.",
69 )
70 }
71 ScopeError::NotFound(_) => Some(
72 "The resource was not found. Verify the address, hash, or token exists on the specified chain.",
73 ),
74 _ => None,
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
87 fn test_display_error_not_found() {
88 let err = ScopeError::NotFound("test resource".into());
89 display_error(&err);
90 }
91
92 #[test]
93 fn test_display_error_invalid_address() {
94 let err = ScopeError::InvalidAddress("0xbad".into());
95 display_error(&err);
96 }
97
98 #[test]
99 fn test_display_error_other() {
100 let err = ScopeError::Other("something went wrong".into());
101 display_error(&err);
102 }
103
104 #[test]
105 fn test_display_error_chain() {
106 let err = ScopeError::Chain("chain error".into());
107 display_error(&err);
108 }
109
110 #[test]
111 fn test_display_error_api() {
112 let err = ScopeError::Api("500 Internal Server Error".into());
113 display_error(&err);
114 }
115
116 #[test]
121 fn test_display_error_styled_tty_not_found() {
122 let err = ScopeError::NotFound("test resource".into());
123 display_error_styled(&err, true);
124 }
125
126 #[test]
127 fn test_display_error_styled_tty_invalid_address() {
128 let err = ScopeError::InvalidAddress("0xbad".into());
129 display_error_styled(&err, true);
130 }
131
132 #[test]
133 fn test_display_error_styled_tty_config() {
134 use crate::error::ConfigError;
135 let err = ScopeError::Config(ConfigError::NotFound {
136 path: std::path::PathBuf::from("/missing"),
137 });
138 display_error_styled(&err, true);
139 }
140
141 #[test]
142 fn test_display_error_styled_tty_network() {
143 let err = ScopeError::Network("timeout".into());
144 display_error_styled(&err, true);
145 }
146
147 #[test]
148 fn test_display_error_styled_tty_api_auth() {
149 let err = ScopeError::Api("401 Unauthorized".into());
150 display_error_styled(&err, true);
151 }
152
153 #[test]
154 fn test_display_error_styled_tty_other_no_hint() {
155 let err = ScopeError::Other("random".into());
156 display_error_styled(&err, true);
157 }
158
159 #[test]
160 fn test_display_error_styled_non_tty() {
161 let err = ScopeError::NotFound("test".into());
162 display_error_styled(&err, false);
163 }
164
165 #[test]
170 fn test_error_suggestion_invalid_address() {
171 let err = ScopeError::InvalidAddress("bad".into());
172 let hint = error_suggestion(&err);
173 assert!(hint.is_some());
174 assert!(hint.unwrap().contains("EVM"));
175 }
176
177 #[test]
178 fn test_error_suggestion_invalid_hash() {
179 let err = ScopeError::InvalidHash("bad".into());
180 let hint = error_suggestion(&err);
181 assert!(hint.is_some());
182 assert!(hint.unwrap().contains("64 hex"));
183 }
184
185 #[test]
186 fn test_error_suggestion_config() {
187 use crate::error::ConfigError;
188 let err = ScopeError::Config(ConfigError::NotFound {
189 path: std::path::PathBuf::from("/missing"),
190 });
191 let hint = error_suggestion(&err);
192 assert!(hint.is_some());
193 assert!(hint.unwrap().contains("scope setup"));
194 }
195
196 #[test]
197 fn test_error_suggestion_network() {
198 let err = ScopeError::Network("timeout".into());
199 let hint = error_suggestion(&err);
200 assert!(hint.is_some());
201 assert!(hint.unwrap().contains("network"));
202 }
203
204 #[test]
205 fn test_error_suggestion_api_auth() {
206 let err = ScopeError::Api("401 Unauthorized".into());
207 let hint = error_suggestion(&err);
208 assert!(hint.is_some());
209 assert!(hint.unwrap().contains("API key"));
210 }
211
212 #[test]
213 fn test_error_suggestion_api_key_keyword() {
214 let err = ScopeError::Api("invalid api key".into());
215 let hint = error_suggestion(&err);
216 assert!(hint.is_some());
217 }
218
219 #[test]
220 fn test_error_suggestion_api_no_auth() {
221 let err = ScopeError::Api("500 Internal Server Error".into());
222 assert!(error_suggestion(&err).is_none());
223 }
224
225 #[test]
226 fn test_error_suggestion_not_found() {
227 let err = ScopeError::NotFound("address".into());
228 let hint = error_suggestion(&err);
229 assert!(hint.is_some());
230 assert!(hint.unwrap().contains("not found"));
231 }
232
233 #[test]
234 fn test_error_suggestion_other_returns_none() {
235 let err = ScopeError::Other("random".into());
236 assert!(error_suggestion(&err).is_none());
237 }
238}