fedimint_api_client/api/
error.rs

1use std::collections::BTreeMap;
2use std::fmt::{self, Debug, Display};
3use std::time::Duration;
4
5use fedimint_core::PeerId;
6use fedimint_core::fmt_utils::AbbreviateJson;
7use fedimint_core::util::FmtCompactAnyhow as _;
8use fedimint_logging::LOG_CLIENT_NET_API;
9#[cfg(target_family = "wasm")]
10use jsonrpsee_wasm_client::{Client as WsClient, WasmClientBuilder as WsClientBuilder};
11use serde::Serialize;
12use thiserror::Error;
13use tracing::{error, trace, warn};
14
15/// An API request error when calling a single federation peer
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum PeerError {
19    /// The response payload was returned successfully but failed to be
20    /// deserialized
21    #[error("Response deserialization error: {0}")]
22    ResponseDeserialization(anyhow::Error),
23
24    /// The request was addressed to an invalid `peer_id`
25    #[error("Invalid peer id: {peer_id}")]
26    InvalidPeerId { peer_id: PeerId },
27
28    /// The endpoint specification for the peer is invalid (e.g. wrong url)
29    #[error("Invalid endpoint")]
30    InvalidEndpoint(anyhow::Error),
31
32    /// Could not connect
33    #[error("Connection failed: {0}")]
34    Connection(anyhow::Error),
35
36    /// Underlying transport failed, in some typical way
37    #[error("Transport error: {0}")]
38    Transport(anyhow::Error),
39
40    /// The rpc id (e.g. jsonrpc method name) was not recognized by the peer
41    ///
42    /// This one is important and sometimes used to detect backward
43    /// compatibility capabilities, so transports should properly support
44    /// it.
45    #[error("Invalid rpc id")]
46    InvalidRpcId(anyhow::Error),
47
48    /// Something about the request we've sent was wrong, should not typically
49    /// happen
50    #[error("Invalid request")]
51    InvalidRequest(anyhow::Error),
52
53    /// Something about the response was wrong, should not typically happen
54    #[error("Invalid response: {0}")]
55    InvalidResponse(anyhow::Error),
56
57    /// Server returned an internal error, suggesting something is wrong with it
58    #[error("Unspecified server error")]
59    ServerError(anyhow::Error),
60
61    /// Some condition on the response this not match
62    ///
63    /// Typically expected, and often used in `FilterMap` query strategy to
64    /// reject responses that don't match some criteria.
65    #[error("Unspecified server error")]
66    ConditionFailed(anyhow::Error),
67
68    /// An internal client error
69    ///
70    /// Things that shouldn't happen (better than panicking), logical errors,
71    /// malfunctions caused by internal issues.
72    #[error("Unspecified internal client")]
73    InternalClientError(anyhow::Error),
74}
75
76impl PeerError {
77    pub fn is_unusual(&self) -> bool {
78        match self {
79            PeerError::ResponseDeserialization(_)
80            | PeerError::InvalidPeerId { .. }
81            | PeerError::InvalidResponse(_)
82            | PeerError::InvalidRpcId(_)
83            | PeerError::InvalidRequest(_)
84            | PeerError::InternalClientError(_)
85            | PeerError::InvalidEndpoint(_)
86            | PeerError::ServerError(_) => true,
87            PeerError::Connection(_) | PeerError::Transport(_) | PeerError::ConditionFailed(_) => {
88                false
89            }
90        }
91    }
92    /// Report errors that are worth reporting
93    ///
94    /// The goal here is to avoid spamming logs with errors that happen commonly
95    /// for all sorts of expected reasons, while printing ones that suggest
96    /// there's a problem.
97    pub fn report_if_unusual(&self, peer_id: PeerId, context: &str) {
98        let unusual = self.is_unusual();
99
100        trace!(target: LOG_CLIENT_NET_API, error = %self, %context, "PeerError");
101
102        if unusual {
103            warn!(target: LOG_CLIENT_NET_API, error = %self,%context, %peer_id, "Unusual PeerError");
104        }
105    }
106}
107
108/// An API request error when calling an entire federation
109///
110/// Generally all Federation errors are retryable.
111#[derive(Debug, Error)]
112pub struct FederationError {
113    pub method: String,
114    pub params: serde_json::Value,
115    /// Higher-level general error
116    ///
117    /// The `general` error should be Some, when the error is not simply peers
118    /// responding with enough errors, but something more global.
119    pub general: Option<anyhow::Error>,
120    pub peer_errors: BTreeMap<PeerId, PeerError>,
121}
122
123impl Display for FederationError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.write_str("Federation rpc error { ")?;
126        f.write_fmt(format_args!("method => {}, ", self.method))?;
127        if let Some(general) = self.general.as_ref() {
128            f.write_fmt(format_args!(
129                "params => {:?}, ",
130                AbbreviateJson(&self.params)
131            ))?;
132            f.write_fmt(format_args!("general => {general}, "))?;
133            if !self.peer_errors.is_empty() {
134                f.write_str(", ")?;
135            }
136        }
137        for (i, (peer, e)) in self.peer_errors.iter().enumerate() {
138            f.write_fmt(format_args!("{peer} => {e:#}"))?;
139            if i != self.peer_errors.len() - 1 {
140                f.write_str(", ")?;
141            }
142        }
143        f.write_str(" }")?;
144        Ok(())
145    }
146}
147
148impl FederationError {
149    pub fn general(
150        method: impl Into<String>,
151        params: impl Serialize,
152        e: impl Into<anyhow::Error>,
153    ) -> FederationError {
154        FederationError {
155            method: method.into(),
156            params: serde_json::to_value(params).unwrap_or_default(),
157            general: Some(e.into()),
158            peer_errors: BTreeMap::default(),
159        }
160    }
161
162    pub(crate) fn peer_errors(
163        method: impl Into<String>,
164        params: impl Serialize,
165        peer_errors: BTreeMap<PeerId, PeerError>,
166    ) -> Self {
167        Self {
168            method: method.into(),
169            params: serde_json::to_value(params).unwrap_or_default(),
170            general: None,
171            peer_errors,
172        }
173    }
174
175    pub fn new_one_peer(
176        peer_id: PeerId,
177        method: impl Into<String>,
178        params: impl Serialize,
179        error: PeerError,
180    ) -> Self {
181        Self {
182            method: method.into(),
183            params: serde_json::to_value(params).expect("Serialization of valid params won't fail"),
184            general: None,
185            peer_errors: [(peer_id, error)].into_iter().collect(),
186        }
187    }
188
189    /// Report any errors
190    pub fn report_if_unusual(&self, context: &str) {
191        if let Some(error) = self.general.as_ref() {
192            // Any general federation errors are unusual
193            warn!(target: LOG_CLIENT_NET_API, err = %error.fmt_compact_anyhow(), %context, "General FederationError");
194        }
195        for (peer_id, e) in &self.peer_errors {
196            e.report_if_unusual(*peer_id, context);
197        }
198    }
199
200    /// Get the general error if any.
201    pub fn get_general_error(&self) -> Option<&anyhow::Error> {
202        self.general.as_ref()
203    }
204
205    /// Get errors from different peers.
206    pub fn get_peer_errors(&self) -> impl Iterator<Item = (PeerId, &PeerError)> {
207        self.peer_errors.iter().map(|(peer, error)| (*peer, error))
208    }
209
210    pub fn any_peer_error_method_not_found(&self) -> bool {
211        self.peer_errors
212            .values()
213            .any(|peer_err| matches!(peer_err, PeerError::InvalidRpcId(_)))
214    }
215}
216
217#[derive(Debug, Error)]
218pub enum OutputOutcomeError {
219    #[error("Response deserialization error: {0}")]
220    ResponseDeserialization(anyhow::Error),
221    #[error("Federation error: {0}")]
222    Federation(#[from] FederationError),
223    #[error("Core error: {0}")]
224    Core(#[from] anyhow::Error),
225    #[error("Transaction rejected: {0}")]
226    Rejected(String),
227    #[error("Invalid output index {out_idx}, larger than {outputs_num} in the transaction")]
228    InvalidVout { out_idx: u64, outputs_num: usize },
229    #[error("Timeout reached after waiting {}s", .0.as_secs())]
230    Timeout(Duration),
231}
232
233impl OutputOutcomeError {
234    pub fn report_if_important(&self) {
235        let important = match self {
236            OutputOutcomeError::Federation(e) => {
237                e.report_if_unusual("OutputOutcome");
238                return;
239            }
240            OutputOutcomeError::Core(_)
241            | OutputOutcomeError::InvalidVout { .. }
242            | OutputOutcomeError::ResponseDeserialization(_) => true,
243            OutputOutcomeError::Rejected(_) | OutputOutcomeError::Timeout(_) => false,
244        };
245
246        trace!(target: LOG_CLIENT_NET_API, error = %self, "OutputOutcomeError");
247
248        if important {
249            warn!(target: LOG_CLIENT_NET_API, error = %self, "Uncommon OutputOutcomeError");
250        }
251    }
252
253    /// Was the transaction rejected (which is final)
254    pub fn is_rejected(&self) -> bool {
255        matches!(
256            self,
257            OutputOutcomeError::Rejected(_) | OutputOutcomeError::InvalidVout { .. }
258        )
259    }
260}