fedimint_api_client/api/
error.rs

1use std::collections::BTreeMap;
2use std::fmt::{self, Debug, Display};
3use std::time::Duration;
4
5use fedimint_connectors::error::ServerError;
6use fedimint_core::PeerId;
7use fedimint_core::fmt_utils::AbbreviateJson;
8use fedimint_core::util::FmtCompactAnyhow as _;
9use fedimint_logging::LOG_CLIENT_NET_API;
10use serde::Serialize;
11use thiserror::Error;
12use tracing::{error, trace, warn};
13
14/// An API request error when calling an entire federation
15///
16/// Generally all Federation errors are retryable.
17#[derive(Debug, Error)]
18pub struct FederationError {
19    pub method: String,
20    pub params: serde_json::Value,
21    /// Higher-level general error
22    ///
23    /// The `general` error should be Some, when the error is not simply peers
24    /// responding with enough errors, but something more global.
25    pub general: Option<anyhow::Error>,
26    pub peer_errors: BTreeMap<PeerId, ServerError>,
27}
28
29impl Display for FederationError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.write_str("Federation rpc error { ")?;
32        f.write_fmt(format_args!("method => {}, ", self.method))?;
33        if let Some(general) = self.general.as_ref() {
34            f.write_fmt(format_args!(
35                "params => {:?}, ",
36                AbbreviateJson(&self.params)
37            ))?;
38            f.write_fmt(format_args!("general => {general}, "))?;
39            if !self.peer_errors.is_empty() {
40                f.write_str(", ")?;
41            }
42        }
43        for (i, (peer, e)) in self.peer_errors.iter().enumerate() {
44            f.write_fmt(format_args!("{peer} => {e:#}"))?;
45            if i != self.peer_errors.len() - 1 {
46                f.write_str(", ")?;
47            }
48        }
49        f.write_str(" }")?;
50        Ok(())
51    }
52}
53
54impl FederationError {
55    pub fn general(
56        method: impl Into<String>,
57        params: impl Serialize,
58        e: impl Into<anyhow::Error>,
59    ) -> FederationError {
60        FederationError {
61            method: method.into(),
62            params: serde_json::to_value(params).unwrap_or_default(),
63            general: Some(e.into()),
64            peer_errors: BTreeMap::default(),
65        }
66    }
67
68    pub(crate) fn peer_errors(
69        method: impl Into<String>,
70        params: impl Serialize,
71        peer_errors: BTreeMap<PeerId, ServerError>,
72    ) -> Self {
73        Self {
74            method: method.into(),
75            params: serde_json::to_value(params).unwrap_or_default(),
76            general: None,
77            peer_errors,
78        }
79    }
80
81    pub fn new_one_peer(
82        peer_id: PeerId,
83        method: impl Into<String>,
84        params: impl Serialize,
85        error: ServerError,
86    ) -> Self {
87        Self {
88            method: method.into(),
89            params: serde_json::to_value(params).expect("Serialization of valid params won't fail"),
90            general: None,
91            peer_errors: [(peer_id, error)].into_iter().collect(),
92        }
93    }
94
95    /// Report any errors
96    pub fn report_if_unusual(&self, context: &str) {
97        if let Some(error) = self.general.as_ref() {
98            // Any general federation errors are unusual
99            warn!(target: LOG_CLIENT_NET_API, err = %error.fmt_compact_anyhow(), %context, "General FederationError");
100        }
101        for (peer_id, e) in &self.peer_errors {
102            e.report_if_unusual(*peer_id, context);
103        }
104    }
105
106    /// Get the general error if any.
107    pub fn get_general_error(&self) -> Option<&anyhow::Error> {
108        self.general.as_ref()
109    }
110
111    /// Get errors from different peers.
112    pub fn get_peer_errors(&self) -> impl Iterator<Item = (PeerId, &ServerError)> {
113        self.peer_errors.iter().map(|(peer, error)| (*peer, error))
114    }
115
116    pub fn any_peer_error_method_not_found(&self) -> bool {
117        self.peer_errors
118            .values()
119            .any(|peer_err| matches!(peer_err, ServerError::InvalidRpcId(_)))
120    }
121}
122
123#[derive(Debug, Error)]
124pub enum OutputOutcomeError {
125    #[error("Response deserialization error: {0}")]
126    ResponseDeserialization(anyhow::Error),
127    #[error("Federation error: {0}")]
128    Federation(#[from] FederationError),
129    #[error("Core error: {0}")]
130    Core(#[from] anyhow::Error),
131    #[error("Transaction rejected: {0}")]
132    Rejected(String),
133    #[error("Invalid output index {out_idx}, larger than {outputs_num} in the transaction")]
134    InvalidVout { out_idx: u64, outputs_num: usize },
135    #[error("Timeout reached after waiting {}s", .0.as_secs())]
136    Timeout(Duration),
137}
138
139impl OutputOutcomeError {
140    pub fn report_if_important(&self) {
141        let important = match self {
142            OutputOutcomeError::Federation(e) => {
143                e.report_if_unusual("OutputOutcome");
144                return;
145            }
146            OutputOutcomeError::Core(_)
147            | OutputOutcomeError::InvalidVout { .. }
148            | OutputOutcomeError::ResponseDeserialization(_) => true,
149            OutputOutcomeError::Rejected(_) | OutputOutcomeError::Timeout(_) => false,
150        };
151
152        trace!(target: LOG_CLIENT_NET_API, error = %self, "OutputOutcomeError");
153
154        if important {
155            warn!(target: LOG_CLIENT_NET_API, error = %self, "Uncommon OutputOutcomeError");
156        }
157    }
158
159    /// Was the transaction rejected (which is final)
160    pub fn is_rejected(&self) -> bool {
161        matches!(
162            self,
163            OutputOutcomeError::Rejected(_) | OutputOutcomeError::InvalidVout { .. }
164        )
165    }
166}