1#![doc = include_str!("../README.md")]
2
3use std::{fmt, io, path::PathBuf, result, time};
4
5use thiserror::Error;
6
7pub type Result<T, E = Error> = result::Result<T, E>;
8
9pub trait OptionData<T> {
14 fn data(self) -> Result<T>;
15}
16
17impl<T> OptionData<T> for Option<T> {
18 #[inline]
19 fn data(self) -> Result<T> {
20 self.ok_or(Error::Internal("data unavailable"))
21 }
22}
23
24#[derive(Debug, Error)]
25pub enum Error {
26 #[error(transparent)]
27 IO(#[from] io::Error),
28
29 #[cfg(feature = "bitcoincore-rpc")]
30 #[error(transparent)]
31 BitcoinRPC(#[from] bitcoincore_rpc::Error),
32
33 #[cfg(feature = "corepc")]
34 #[error(transparent)]
35 CorepcRPC(#[from] corepc_client::client_sync::Error),
36
37 #[cfg(feature = "jiff")]
38 #[error(transparent)]
39 Jiff(#[from] jiff::Error),
40
41 #[cfg(feature = "fjall")]
42 #[error(transparent)]
43 Fjall(#[from] fjall::Error),
44
45 #[cfg(feature = "vecdb")]
46 #[error(transparent)]
47 VecDB(#[from] vecdb::Error),
48
49 #[cfg(feature = "vecdb")]
50 #[error(transparent)]
51 RawDB(#[from] vecdb::RawDBError),
52
53 #[cfg(feature = "ureq")]
54 #[error(transparent)]
55 Ureq(#[from] ureq::Error),
56
57 #[error(transparent)]
58 SystemTimeError(#[from] time::SystemTimeError),
59
60 #[cfg(feature = "bitcoin")]
61 #[error(transparent)]
62 BitcoinConsensusEncode(#[from] bitcoin::consensus::encode::Error),
63
64 #[cfg(feature = "bitcoin")]
65 #[error(transparent)]
66 BitcoinBip34Error(#[from] bitcoin::block::Bip34Error),
67
68 #[cfg(feature = "bitcoin")]
69 #[error(transparent)]
70 BitcoinHexError(#[from] bitcoin::consensus::encode::FromHexError),
71
72 #[cfg(feature = "bitcoin")]
73 #[error(transparent)]
74 BitcoinFromScriptError(#[from] bitcoin::address::FromScriptError),
75
76 #[cfg(feature = "bitcoin")]
77 #[error(transparent)]
78 BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError),
79
80 #[cfg(feature = "pco")]
81 #[error(transparent)]
82 Pco(#[from] pco::errors::PcoError),
83
84 #[cfg(feature = "serde_json")]
85 #[error(transparent)]
86 SerdeJSON(#[from] serde_json::Error),
87
88 #[cfg(feature = "tokio")]
89 #[error(transparent)]
90 TokioJoin(#[from] tokio::task::JoinError),
91
92 #[error("ZeroCopy error")]
93 ZeroCopyError,
94
95 #[error("Wrong length, expected: {expected}, received: {received}")]
96 WrongLength { expected: usize, received: usize },
97
98 #[error("Wrong address type")]
99 WrongAddrType,
100
101 #[error("Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater")]
102 UnindexableDate,
103
104 #[error("Quick cache error")]
105 QuickCacheError,
106
107 #[error("The provided address appears to be invalid")]
108 InvalidAddr,
109
110 #[error("Invalid network")]
111 InvalidNetwork,
112
113 #[error("The provided TXID appears to be invalid")]
114 InvalidTxid,
115
116 #[error("Mempool data is not available")]
117 MempoolNotAvailable,
118
119 #[error("Address not found in the blockchain (no transaction history)")]
120 UnknownAddr,
121
122 #[error("Failed to find the TXID in the blockchain")]
123 UnknownTxid,
124
125 #[error("Unsupported type ({0})")]
126 UnsupportedType(String),
127
128 #[error("{0}")]
130 NotFound(String),
131
132 #[error("{0}")]
133 OutOfRange(String),
134
135 #[error("{0}")]
136 Parse(String),
137
138 #[error("Internal error: {0}")]
139 Internal(&'static str),
140
141 #[error("Authentication failed")]
142 AuthFailed,
143
144 #[error("{0}")]
146 SeriesNotFound(SeriesNotFound),
147
148 #[error("'{series}' doesn't support the requested index. Try: {supported}")]
149 SeriesUnsupportedIndex { series: String, supported: String },
150
151 #[error("No series specified")]
152 NoSeries,
153
154 #[error("No data available")]
155 NoData,
156
157 #[error("Request weight {requested} exceeds maximum {max}")]
158 WeightExceeded { requested: usize, max: usize },
159
160 #[error("Deserialization error: {0}")]
161 Deserialization(String),
162
163 #[error("Fetch failed after retries: {0}")]
164 FetchFailed(String),
165
166 #[error("HTTP {status}: {url}")]
167 HttpStatus { status: u16, url: String },
168
169 #[error("Version mismatch at {path:?}: expected {expected}, found {found}")]
170 VersionMismatch {
171 path: PathBuf,
172 expected: usize,
173 found: usize,
174 },
175}
176
177impl Error {
178 #[cfg(feature = "vecdb")]
181 pub fn is_lock_error(&self) -> bool {
182 matches!(self, Error::VecDB(e) if e.is_lock_error())
183 }
184
185 #[cfg(feature = "vecdb")]
188 pub fn is_data_error(&self) -> bool {
189 matches!(self, Error::VecDB(e) if e.is_data_error())
190 || matches!(self, Error::VersionMismatch { .. })
191 }
192
193 pub fn is_network_permanently_blocked(&self) -> bool {
197 match self {
198 #[cfg(feature = "ureq")]
199 Error::Ureq(e) => is_ureq_error_permanent(e),
200 Error::IO(e) => is_io_error_permanent(e),
201 Error::HttpStatus { status, .. } => *status == 403,
203 _ => false,
205 }
206 }
207}
208
209#[cfg(feature = "ureq")]
210fn is_ureq_error_permanent(e: &ureq::Error) -> bool {
211 let msg = format!("{:?}", e);
212 msg.contains("nodename nor servname")
213 || msg.contains("Name or service not known")
214 || msg.contains("No such host")
215 || msg.contains("connection refused")
216 || msg.contains("Connection refused")
217 || msg.contains("certificate")
218 || msg.contains("SSL")
219 || msg.contains("TLS")
220 || msg.contains("handshake")
221}
222
223fn is_io_error_permanent(e: &std::io::Error) -> bool {
224 use std::io::ErrorKind::*;
225 match e.kind() {
226 ConnectionRefused | PermissionDenied | AddrNotAvailable => true,
228 _ => {
230 let msg = e.to_string();
231 msg.contains("nodename nor servname")
232 || msg.contains("Name or service not known")
233 || msg.contains("No such host")
234 }
235 }
236}
237
238#[derive(Debug)]
239pub struct SeriesNotFound {
240 pub series: String,
241 pub suggestions: Vec<String>,
242 pub total_matches: usize,
243}
244
245impl SeriesNotFound {
246 pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
247 let total_matches = all_matches.len();
248 let suggestions = all_matches.into_iter().take(3).collect();
249 if series.len() > 100 {
250 series.truncate(100);
251 series.push_str("...");
252 }
253 Self {
254 series,
255 suggestions,
256 total_matches,
257 }
258 }
259}
260
261impl fmt::Display for SeriesNotFound {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 write!(f, "'{}' not found", self.series)?;
264
265 if self.suggestions.is_empty() {
266 return Ok(());
267 }
268
269 let quoted: Vec<_> = self.suggestions.iter().map(|s| format!("'{s}'")).collect();
270 write!(f, ", did you mean {}?", quoted.join(", "))?;
271
272 let remaining = self.total_matches.saturating_sub(self.suggestions.len());
273 if remaining > 0 {
274 write!(
275 f,
276 " ({remaining} more — /api/series/search?q={} for all)",
277 self.series
278 )?;
279 }
280
281 Ok(())
282 }
283}