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