fedimint_api_client/api/
error.rs

1use std::collections::BTreeMap;
2use std::fmt::{self, Debug, Display};
3use std::time::Duration;
4
5use fedimint_core::fmt_utils::AbbreviateJson;
6use fedimint_core::PeerId;
7use fedimint_logging::LOG_CLIENT_NET_API;
8use jsonrpsee_core::client::Error as JsonRpcClientError;
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)]
17pub enum PeerError {
18    #[error("Response deserialization error: {0}")]
19    ResponseDeserialization(anyhow::Error),
20    #[error("Invalid peer id: {peer_id}")]
21    InvalidPeerId { peer_id: PeerId },
22    #[error("Rpc error: {0}")]
23    Rpc(#[from] JsonRpcClientError),
24    #[error("Invalid response: {0}")]
25    InvalidResponse(String),
26}
27
28impl PeerError {
29    /// Report errors that are worth reporting
30    ///
31    /// The goal here is to avoid spamming logs with errors that happen commonly
32    /// for all sorts of expected reasons, while printing ones that suggest
33    /// there's a problem.
34    pub fn report_if_important(&self, peer_id: PeerId) {
35        let important = match self {
36            PeerError::ResponseDeserialization(_)
37            | PeerError::InvalidPeerId { .. }
38            | PeerError::InvalidResponse(_) => true,
39            PeerError::Rpc(rpc_e) => match rpc_e {
40                // TODO: Does this cover all retryable cases?
41                JsonRpcClientError::Transport(_) | JsonRpcClientError::RequestTimeout => false,
42                JsonRpcClientError::RestartNeeded(_)
43                | JsonRpcClientError::Call(_)
44                | JsonRpcClientError::ParseError(_)
45                | JsonRpcClientError::InvalidSubscriptionId
46                | JsonRpcClientError::InvalidRequestId(_)
47                | JsonRpcClientError::Custom(_)
48                | JsonRpcClientError::HttpNotImplemented
49                | JsonRpcClientError::EmptyBatchRequest(_)
50                | JsonRpcClientError::RegisterMethod(_) => true,
51            },
52        };
53
54        trace!(target: LOG_CLIENT_NET_API, error = %self, "PeerError");
55
56        if important {
57            warn!(target: LOG_CLIENT_NET_API, error = %self, %peer_id, "Unusual PeerError");
58        }
59    }
60}
61
62/// An API request error when calling an entire federation
63///
64/// Generally all Federation errors are retryable.
65#[derive(Debug, Error)]
66pub struct FederationError {
67    pub method: String,
68    pub params: serde_json::Value,
69    pub general: Option<anyhow::Error>,
70    pub peers: BTreeMap<PeerId, PeerError>,
71}
72
73impl Display for FederationError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str("Federation rpc error {")?;
76        if let Some(general) = self.general.as_ref() {
77            f.write_fmt(format_args!("method => {}, ", self.method))?;
78            f.write_fmt(format_args!(
79                "params => {:?}, ",
80                AbbreviateJson(&self.params)
81            ))?;
82            f.write_fmt(format_args!("general => {general}, "))?;
83            if !self.peers.is_empty() {
84                f.write_str(", ")?;
85            }
86        }
87        for (i, (peer, e)) in self.peers.iter().enumerate() {
88            f.write_fmt(format_args!("{peer} => {e})"))?;
89            if i == self.peers.len() - 1 {
90                f.write_str(", ")?;
91            }
92        }
93        f.write_str("}")?;
94        Ok(())
95    }
96}
97
98impl FederationError {
99    pub fn general(
100        method: impl Into<String>,
101        params: impl Serialize,
102        e: impl Into<anyhow::Error>,
103    ) -> FederationError {
104        FederationError {
105            method: method.into(),
106            params: serde_json::to_value(params).unwrap_or_default(),
107            general: Some(e.into()),
108            peers: BTreeMap::default(),
109        }
110    }
111
112    pub fn new_one_peer(
113        peer_id: PeerId,
114        method: impl Into<String>,
115        params: impl Serialize,
116        error: PeerError,
117    ) -> Self {
118        Self {
119            method: method.into(),
120            params: serde_json::to_value(params).expect("Serialization of valid params won't fail"),
121            general: None,
122            peers: [(peer_id, error)].into_iter().collect(),
123        }
124    }
125
126    /// Report any errors
127    pub fn report_if_important(&self) {
128        if let Some(error) = self.general.as_ref() {
129            warn!(target: LOG_CLIENT_NET_API, %error, "General FederationError");
130        }
131        for (peer_id, e) in &self.peers {
132            e.report_if_important(*peer_id);
133        }
134    }
135
136    /// Get the general error if any.
137    pub fn get_general_error(&self) -> Option<&anyhow::Error> {
138        self.general.as_ref()
139    }
140
141    /// Get errors from different peers.
142    pub fn get_peer_errors(&self) -> impl Iterator<Item = (PeerId, &PeerError)> {
143        self.peers.iter().map(|(peer, error)| (*peer, error))
144    }
145}
146
147#[derive(Debug, Error)]
148pub enum OutputOutcomeError {
149    #[error("Response deserialization error: {0}")]
150    ResponseDeserialization(anyhow::Error),
151    #[error("Federation error: {0}")]
152    Federation(#[from] FederationError),
153    #[error("Core error: {0}")]
154    Core(#[from] anyhow::Error),
155    #[error("Transaction rejected: {0}")]
156    Rejected(String),
157    #[error("Invalid output index {out_idx}, larger than {outputs_num} in the transaction")]
158    InvalidVout { out_idx: u64, outputs_num: usize },
159    #[error("Timeout reached after waiting {}s", .0.as_secs())]
160    Timeout(Duration),
161}
162
163impl OutputOutcomeError {
164    pub fn report_if_important(&self) {
165        let important = match self {
166            OutputOutcomeError::Federation(e) => {
167                e.report_if_important();
168                return;
169            }
170            OutputOutcomeError::Core(_)
171            | OutputOutcomeError::InvalidVout { .. }
172            | OutputOutcomeError::ResponseDeserialization(_) => true,
173            OutputOutcomeError::Rejected(_) | OutputOutcomeError::Timeout(_) => false,
174        };
175
176        trace!(target: LOG_CLIENT_NET_API, error = %self, "OutputOutcomeError");
177
178        if important {
179            warn!(target: LOG_CLIENT_NET_API, error = %self, "Uncommon OutputOutcomeError");
180        }
181    }
182
183    /// Was the transaction rejected (which is final)
184    pub fn is_rejected(&self) -> bool {
185        matches!(
186            self,
187            OutputOutcomeError::Rejected(_) | OutputOutcomeError::InvalidVout { .. }
188        )
189    }
190}