Skip to main content

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::config::FederationId;
8use fedimint_core::core::ModuleInstanceId;
9use fedimint_core::encoding::DecodeError;
10use fedimint_core::fmt_utils::AbbreviateJson;
11use fedimint_core::util::FmtCompact as _;
12use fedimint_logging::LOG_CLIENT_NET_API;
13use serde::Serialize;
14use thiserror::Error;
15use tracing::{trace, warn};
16
17/// A federation-wide failure that is not simply peers returning errors.
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum FederationGeneralError {
21    /// The call is an admin call, but this api handle does not know which peer
22    /// it speaks for.
23    #[error("Admin peer id not set")]
24    AdminPeerIdNotSet,
25
26    /// Not enough peers agreed for the request to produce an answer.
27    #[error("{message}")]
28    ThresholdFailed { message: String },
29
30    /// A peer answered, but with something the caller cannot use.
31    #[error("{message}")]
32    UnexpectedResponse { message: String },
33
34    /// A peer's response was well-formed json but did not decode.
35    #[error("Failed to decode a peer's response")]
36    Decode(#[from] DecodeError),
37
38    /// The signed session outcome did not verify against the broadcast keys.
39    #[error("Invalid signature")]
40    InvalidSignature,
41}
42
43/// An API request error when calling an entire federation
44///
45/// Generally all Federation errors are retryable.
46#[derive(Debug, Error)]
47pub struct FederationError {
48    pub method: String,
49    pub params: serde_json::Value,
50    /// Higher-level general error
51    ///
52    /// The `general` error should be Some, when the error is not simply peers
53    /// responding with enough errors, but something more global.
54    pub general: Option<FederationGeneralError>,
55    pub peer_errors: BTreeMap<PeerId, ServerError>,
56}
57
58impl Display for FederationError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str("Federation rpc error { ")?;
61        f.write_fmt(format_args!("method => {}, ", self.method))?;
62        if let Some(general) = self.general.as_ref() {
63            f.write_fmt(format_args!(
64                "params => {:?}, ",
65                AbbreviateJson(&self.params)
66            ))?;
67            f.write_fmt(format_args!("general => {}, ", general.fmt_compact()))?;
68            if !self.peer_errors.is_empty() {
69                f.write_str(", ")?;
70            }
71        }
72        for (i, (peer, e)) in self.peer_errors.iter().enumerate() {
73            f.write_fmt(format_args!("{peer} => {e:#}"))?;
74            if i != self.peer_errors.len() - 1 {
75                f.write_str(", ")?;
76            }
77        }
78        f.write_str(" }")?;
79        Ok(())
80    }
81}
82
83impl FederationError {
84    pub fn general(
85        method: impl Into<String>,
86        params: impl Serialize,
87        e: FederationGeneralError,
88    ) -> FederationError {
89        FederationError {
90            method: method.into(),
91            params: serde_json::to_value(params).unwrap_or_default(),
92            general: Some(e),
93            peer_errors: BTreeMap::default(),
94        }
95    }
96
97    pub(crate) fn peer_errors(
98        method: impl Into<String>,
99        params: impl Serialize,
100        peer_errors: BTreeMap<PeerId, ServerError>,
101    ) -> Self {
102        Self {
103            method: method.into(),
104            params: serde_json::to_value(params).unwrap_or_default(),
105            general: None,
106            peer_errors,
107        }
108    }
109
110    pub fn new_one_peer(
111        peer_id: PeerId,
112        method: impl Into<String>,
113        params: impl Serialize,
114        error: ServerError,
115    ) -> Self {
116        Self {
117            method: method.into(),
118            params: serde_json::to_value(params).expect("Serialization of valid params won't fail"),
119            general: None,
120            peer_errors: [(peer_id, error)].into_iter().collect(),
121        }
122    }
123
124    /// Report any errors
125    pub fn report_if_unusual(&self, context: &str) {
126        if let Some(error) = self.general.as_ref() {
127            // Any general federation errors are unusual
128            warn!(target: LOG_CLIENT_NET_API, err = %error.fmt_compact(), %context, "General FederationError");
129        }
130        for (peer_id, e) in &self.peer_errors {
131            e.report_if_unusual(*peer_id, context);
132        }
133    }
134
135    /// The general error, if any.
136    pub fn get_general_error(&self) -> Option<&FederationGeneralError> {
137        self.general.as_ref()
138    }
139
140    /// Get errors from different peers.
141    pub fn get_peer_errors(&self) -> impl Iterator<Item = (PeerId, &ServerError)> {
142        self.peer_errors.iter().map(|(peer, error)| (*peer, error))
143    }
144
145    pub fn any_peer_error_method_not_found(&self) -> bool {
146        self.peer_errors
147            .values()
148            .any(|peer_err| matches!(peer_err, ServerError::InvalidRpcId(_)))
149    }
150}
151
152#[derive(Debug, Error)]
153#[non_exhaustive]
154pub enum OutputOutcomeError {
155    /// The outcome bytes the federation returned could not be decoded.
156    #[error("Failed to decode the output outcome")]
157    ResponseDeserialization(#[from] DecodeError),
158    /// The outcome decoded into a different type than the caller asked for.
159    #[error("Output outcome of module instance {module_instance_id} is not a {expected_type}")]
160    WrongOutcomeType {
161        module_instance_id: ModuleInstanceId,
162        expected_type: &'static str,
163    },
164    /// The request to the federation failed.
165    #[error("Federation error")]
166    Federation(#[from] FederationError),
167    /// The transaction that would have produced the outcome was rejected.
168    #[error("Transaction rejected: {0}")]
169    Rejected(String),
170    /// The transaction has no output at the index the caller asked about.
171    #[error("Invalid output index {out_idx}, larger than {outputs_num} in the transaction")]
172    InvalidVout { out_idx: u64, outputs_num: usize },
173    /// The outcome did not become available within the time the caller allowed.
174    #[error("Timeout reached after waiting {}s", .0.as_secs())]
175    Timeout(Duration),
176}
177
178impl OutputOutcomeError {
179    pub fn report_if_important(&self) {
180        let important = match self {
181            OutputOutcomeError::Federation(e) => {
182                e.report_if_unusual("OutputOutcome");
183                return;
184            }
185            OutputOutcomeError::WrongOutcomeType { .. }
186            | OutputOutcomeError::InvalidVout { .. }
187            | OutputOutcomeError::ResponseDeserialization(_) => true,
188            OutputOutcomeError::Rejected(_) | OutputOutcomeError::Timeout(_) => false,
189        };
190
191        trace!(target: LOG_CLIENT_NET_API, error = %self.fmt_compact(), "OutputOutcomeError");
192
193        if important {
194            warn!(
195                target: LOG_CLIENT_NET_API,
196                error = %self.fmt_compact(),
197                "Uncommon OutputOutcomeError"
198            );
199        }
200    }
201
202    /// Was the transaction rejected (which is final)
203    pub fn is_rejected(&self) -> bool {
204        matches!(
205            self,
206            OutputOutcomeError::Rejected(_) | OutputOutcomeError::InvalidVout { .. }
207        )
208    }
209}
210
211/// A failure to download a federation's client config.
212#[derive(Debug, Error)]
213#[non_exhaustive]
214pub enum ClientConfigDownloadError {
215    /// The federation could not be asked for its config.
216    #[error("Failed to request the client config")]
217    Federation(#[from] FederationError),
218
219    /// The config the federation returned belongs to a different federation
220    /// than the invite code names.
221    #[error("Obtained client config has federation id {found}, expected {expected}")]
222    FederationIdMismatch {
223        expected: FederationId,
224        found: FederationId,
225    },
226}