#![doc = include_str!("../README.md")]
use std::{fmt, io, path::PathBuf, result, time};
use thiserror::Error;
pub type Result<T, E = Error> = result::Result<T, E>;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
IO(#[from] io::Error),
#[cfg(feature = "bitcoincore-rpc")]
#[error(transparent)]
BitcoinRPC(#[from] bitcoincore_rpc::Error),
#[cfg(feature = "corepc")]
#[error(transparent)]
CorepcRPC(#[from] corepc_client::client_sync::Error),
#[cfg(feature = "jiff")]
#[error(transparent)]
Jiff(#[from] jiff::Error),
#[cfg(feature = "fjall")]
#[error(transparent)]
Fjall(#[from] fjall::Error),
#[cfg(feature = "vecdb")]
#[error(transparent)]
VecDB(#[from] vecdb::Error),
#[cfg(feature = "vecdb")]
#[error(transparent)]
RawDB(#[from] vecdb::RawDBError),
#[cfg(feature = "ureq")]
#[error(transparent)]
Ureq(#[from] ureq::Error),
#[error(transparent)]
SystemTimeError(#[from] time::SystemTimeError),
#[cfg(feature = "bitcoin")]
#[error(transparent)]
BitcoinConsensusEncode(#[from] bitcoin::consensus::encode::Error),
#[cfg(feature = "bitcoin")]
#[error(transparent)]
BitcoinBip34Error(#[from] bitcoin::block::Bip34Error),
#[cfg(feature = "bitcoin")]
#[error(transparent)]
BitcoinHexError(#[from] bitcoin::consensus::encode::FromHexError),
#[cfg(feature = "bitcoin")]
#[error(transparent)]
BitcoinFromScriptError(#[from] bitcoin::address::FromScriptError),
#[cfg(feature = "bitcoin")]
#[error(transparent)]
BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError),
#[cfg(feature = "pco")]
#[error(transparent)]
Pco(#[from] pco::errors::PcoError),
#[cfg(feature = "serde_json")]
#[error(transparent)]
SerdeJSON(#[from] serde_json::Error),
#[cfg(feature = "tokio")]
#[error(transparent)]
TokioJoin(#[from] tokio::task::JoinError),
#[error("ZeroCopy error")]
ZeroCopyError,
#[error("Wrong length, expected: {expected}, received: {received}")]
WrongLength { expected: usize, received: usize },
#[error("Wrong address type")]
WrongAddrType,
#[error("Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater")]
UnindexableDate,
#[error("Quick cache error")]
QuickCacheError,
#[error("The provided address appears to be invalid")]
InvalidAddr,
#[error("Invalid network")]
InvalidNetwork,
#[error("The provided TXID appears to be invalid")]
InvalidTxid,
#[error("Mempool data is not available")]
MempoolNotAvailable,
#[error("Address not found in the blockchain (no transaction history)")]
UnknownAddr,
#[error("Failed to find the TXID in the blockchain")]
UnknownTxid,
#[error("Unsupported type ({0})")]
UnsupportedType(String),
#[error("{0}")]
NotFound(String),
#[error("{0}")]
OutOfRange(String),
#[error("{0}")]
Parse(String),
#[error("Internal error: {0}")]
Internal(&'static str),
#[error("Authentication failed")]
AuthFailed,
#[error("{0}")]
SeriesNotFound(SeriesNotFound),
#[error("'{series}' doesn't support the requested index. Try: {supported}")]
SeriesUnsupportedIndex { series: String, supported: String },
#[error("No series specified")]
NoSeries,
#[error("No data available")]
NoData,
#[error("Request weight {requested} exceeds maximum {max}")]
WeightExceeded { requested: usize, max: usize },
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Fetch failed after retries: {0}")]
FetchFailed(String),
#[error("HTTP {status}: {url}")]
HttpStatus { status: u16, url: String },
#[error("Version mismatch at {path:?}: expected {expected}, found {found}")]
VersionMismatch {
path: PathBuf,
expected: usize,
found: usize,
},
}
impl Error {
#[cfg(feature = "vecdb")]
pub fn is_lock_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_lock_error())
}
#[cfg(feature = "vecdb")]
pub fn is_data_error(&self) -> bool {
matches!(self, Error::VecDB(e) if e.is_data_error())
|| matches!(self, Error::VersionMismatch { .. })
}
pub fn is_network_permanently_blocked(&self) -> bool {
match self {
#[cfg(feature = "ureq")]
Error::Ureq(e) => is_ureq_error_permanent(e),
Error::IO(e) => is_io_error_permanent(e),
Error::HttpStatus { status, .. } => *status == 403,
_ => false,
}
}
}
#[cfg(feature = "ureq")]
fn is_ureq_error_permanent(e: &ureq::Error) -> bool {
let msg = format!("{:?}", e);
msg.contains("nodename nor servname")
|| msg.contains("Name or service not known")
|| msg.contains("No such host")
|| msg.contains("connection refused")
|| msg.contains("Connection refused")
|| msg.contains("certificate")
|| msg.contains("SSL")
|| msg.contains("TLS")
|| msg.contains("handshake")
}
fn is_io_error_permanent(e: &std::io::Error) -> bool {
use std::io::ErrorKind::*;
match e.kind() {
ConnectionRefused | PermissionDenied | AddrNotAvailable => true,
_ => {
let msg = e.to_string();
msg.contains("nodename nor servname")
|| msg.contains("Name or service not known")
|| msg.contains("No such host")
}
}
}
#[derive(Debug)]
pub struct SeriesNotFound {
pub series: String,
pub suggestions: Vec<String>,
pub total_matches: usize,
}
impl SeriesNotFound {
pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
let total_matches = all_matches.len();
let suggestions = all_matches.into_iter().take(3).collect();
if series.len() > 100 {
series.truncate(100);
series.push_str("...");
}
Self {
series,
suggestions,
total_matches,
}
}
}
impl fmt::Display for SeriesNotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "'{}' not found", self.series)?;
if self.suggestions.is_empty() {
return Ok(());
}
let quoted: Vec<_> = self.suggestions.iter().map(|s| format!("'{s}'")).collect();
write!(f, ", did you mean {}?", quoted.join(", "))?;
let remaining = self.total_matches.saturating_sub(self.suggestions.len());
if remaining > 0 {
write!(
f,
" ({remaining} more — /api/series/search?q={} for all)",
self.series
)?;
}
Ok(())
}
}