fedimint-api-client 0.13.0-beta.0

fedimint-api-client provides common code used by client.
Documentation
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Display};
use std::time::Duration;

use fedimint_connectors::error::ServerError;
use fedimint_core::PeerId;
use fedimint_core::config::FederationId;
use fedimint_core::core::ModuleInstanceId;
use fedimint_core::encoding::DecodeError;
use fedimint_core::fmt_utils::AbbreviateJson;
use fedimint_core::util::FmtCompact as _;
use fedimint_logging::LOG_CLIENT_NET_API;
use serde::Serialize;
use thiserror::Error;
use tracing::{trace, warn};

/// A federation-wide failure that is not simply peers returning errors.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FederationGeneralError {
    /// The call is an admin call, but this api handle does not know which peer
    /// it speaks for.
    #[error("Admin peer id not set")]
    AdminPeerIdNotSet,

    /// Not enough peers agreed for the request to produce an answer.
    #[error("{message}")]
    ThresholdFailed { message: String },

    /// A peer answered, but with something the caller cannot use.
    #[error("{message}")]
    UnexpectedResponse { message: String },

    /// A peer's response was well-formed json but did not decode.
    #[error("Failed to decode a peer's response")]
    Decode(#[from] DecodeError),

    /// The signed session outcome did not verify against the broadcast keys.
    #[error("Invalid signature")]
    InvalidSignature,
}

/// An API request error when calling an entire federation
///
/// Generally all Federation errors are retryable.
#[derive(Debug, Error)]
pub struct FederationError {
    pub method: String,
    pub params: serde_json::Value,
    /// Higher-level general error
    ///
    /// The `general` error should be Some, when the error is not simply peers
    /// responding with enough errors, but something more global.
    pub general: Option<FederationGeneralError>,
    pub peer_errors: BTreeMap<PeerId, ServerError>,
}

impl Display for FederationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Federation rpc error { ")?;
        f.write_fmt(format_args!("method => {}, ", self.method))?;
        if let Some(general) = self.general.as_ref() {
            f.write_fmt(format_args!(
                "params => {:?}, ",
                AbbreviateJson(&self.params)
            ))?;
            f.write_fmt(format_args!("general => {}, ", general.fmt_compact()))?;
            if !self.peer_errors.is_empty() {
                f.write_str(", ")?;
            }
        }
        for (i, (peer, e)) in self.peer_errors.iter().enumerate() {
            f.write_fmt(format_args!("{peer} => {e:#}"))?;
            if i != self.peer_errors.len() - 1 {
                f.write_str(", ")?;
            }
        }
        f.write_str(" }")?;
        Ok(())
    }
}

impl FederationError {
    pub fn general(
        method: impl Into<String>,
        params: impl Serialize,
        e: FederationGeneralError,
    ) -> FederationError {
        FederationError {
            method: method.into(),
            params: serde_json::to_value(params).unwrap_or_default(),
            general: Some(e),
            peer_errors: BTreeMap::default(),
        }
    }

    pub(crate) fn peer_errors(
        method: impl Into<String>,
        params: impl Serialize,
        peer_errors: BTreeMap<PeerId, ServerError>,
    ) -> Self {
        Self {
            method: method.into(),
            params: serde_json::to_value(params).unwrap_or_default(),
            general: None,
            peer_errors,
        }
    }

    pub fn new_one_peer(
        peer_id: PeerId,
        method: impl Into<String>,
        params: impl Serialize,
        error: ServerError,
    ) -> Self {
        Self {
            method: method.into(),
            params: serde_json::to_value(params).expect("Serialization of valid params won't fail"),
            general: None,
            peer_errors: [(peer_id, error)].into_iter().collect(),
        }
    }

    /// Report any errors
    pub fn report_if_unusual(&self, context: &str) {
        if let Some(error) = self.general.as_ref() {
            // Any general federation errors are unusual
            warn!(target: LOG_CLIENT_NET_API, err = %error.fmt_compact(), %context, "General FederationError");
        }
        for (peer_id, e) in &self.peer_errors {
            e.report_if_unusual(*peer_id, context);
        }
    }

    /// The general error, if any.
    pub fn get_general_error(&self) -> Option<&FederationGeneralError> {
        self.general.as_ref()
    }

    /// Get errors from different peers.
    pub fn get_peer_errors(&self) -> impl Iterator<Item = (PeerId, &ServerError)> {
        self.peer_errors.iter().map(|(peer, error)| (*peer, error))
    }

    pub fn any_peer_error_method_not_found(&self) -> bool {
        self.peer_errors
            .values()
            .any(|peer_err| matches!(peer_err, ServerError::InvalidRpcId(_)))
    }
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum OutputOutcomeError {
    /// The outcome bytes the federation returned could not be decoded.
    #[error("Failed to decode the output outcome")]
    ResponseDeserialization(#[from] DecodeError),
    /// The outcome decoded into a different type than the caller asked for.
    #[error("Output outcome of module instance {module_instance_id} is not a {expected_type}")]
    WrongOutcomeType {
        module_instance_id: ModuleInstanceId,
        expected_type: &'static str,
    },
    /// The request to the federation failed.
    #[error("Federation error")]
    Federation(#[from] FederationError),
    /// The transaction that would have produced the outcome was rejected.
    #[error("Transaction rejected: {0}")]
    Rejected(String),
    /// The transaction has no output at the index the caller asked about.
    #[error("Invalid output index {out_idx}, larger than {outputs_num} in the transaction")]
    InvalidVout { out_idx: u64, outputs_num: usize },
    /// The outcome did not become available within the time the caller allowed.
    #[error("Timeout reached after waiting {}s", .0.as_secs())]
    Timeout(Duration),
}

impl OutputOutcomeError {
    pub fn report_if_important(&self) {
        let important = match self {
            OutputOutcomeError::Federation(e) => {
                e.report_if_unusual("OutputOutcome");
                return;
            }
            OutputOutcomeError::WrongOutcomeType { .. }
            | OutputOutcomeError::InvalidVout { .. }
            | OutputOutcomeError::ResponseDeserialization(_) => true,
            OutputOutcomeError::Rejected(_) | OutputOutcomeError::Timeout(_) => false,
        };

        trace!(target: LOG_CLIENT_NET_API, error = %self.fmt_compact(), "OutputOutcomeError");

        if important {
            warn!(
                target: LOG_CLIENT_NET_API,
                error = %self.fmt_compact(),
                "Uncommon OutputOutcomeError"
            );
        }
    }

    /// Was the transaction rejected (which is final)
    pub fn is_rejected(&self) -> bool {
        matches!(
            self,
            OutputOutcomeError::Rejected(_) | OutputOutcomeError::InvalidVout { .. }
        )
    }
}

/// A failure to download a federation's client config.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ClientConfigDownloadError {
    /// The federation could not be asked for its config.
    #[error("Failed to request the client config")]
    Federation(#[from] FederationError),

    /// The config the federation returned belongs to a different federation
    /// than the invite code names.
    #[error("Obtained client config has federation id {found}, expected {expected}")]
    FederationIdMismatch {
        expected: FederationId,
        found: FederationId,
    },
}