xenith-core 0.1.0

Transport-agnostic traits, types, and errors for xenith cross-chain state sync
Documentation
use crate::{ChainId, MessageId, StateKey};
use thiserror::Error;

/// Unified error type for all xenith operations.
///
/// # Example
///
/// ```
/// use xenith_core::{XenithError, ChainId};
/// let e = XenithError::UnsupportedChain(ChainId::from(9999));
/// assert_eq!(e.to_string(), "chain 9999 is not supported by this transport");
/// ```
#[derive(Clone, Debug, Error)]
pub enum XenithError {
    /// The `chain` field identifies which chain the transport was interacting
    /// with when the error occurred. Use `ChainId(0)` only when the chain
    /// cannot be determined (e.g., during initial connection before chain
    /// ID is known, or when parsing a response that carries no chain context).
    #[error("transport error on chain {chain}: {message}")]
    Transport { chain: ChainId, message: String },

    #[error("state diverged for key {key} across chains: {chains:?}")]
    Divergence { key: StateKey, chains: Vec<ChainId> },

    #[error("message {id} timed out after {elapsed_secs}s")]
    Timeout { id: MessageId, elapsed_secs: u64 },

    #[error("insufficient fee: required {required}, provided {provided}")]
    InsufficientFee { required: String, provided: String },

    #[error("chain {0} is not supported by this transport")]
    UnsupportedChain(ChainId),

    #[error("state store error: {0}")]
    StoreError(String),

    #[error("serialization error: {0}")]
    Serialization(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn transport_message() {
        let e = XenithError::Transport {
            chain: ChainId(1),
            message: "connection refused".into(),
        };
        assert_eq!(
            e.to_string(),
            "transport error on chain 1: connection refused"
        );
    }

    #[test]
    fn divergence_message() {
        let e = XenithError::Divergence {
            key: StateKey::new("proto", "pool", "0xabc"),
            chains: vec![ChainId(1), ChainId(42161)],
        };
        assert_eq!(
            e.to_string(),
            "state diverged for key proto.pool.0xabc across chains: [ChainId(1), ChainId(42161)]"
        );
    }

    #[test]
    fn timeout_message() {
        let e = XenithError::Timeout {
            id: MessageId(7),
            elapsed_secs: 30,
        };
        assert_eq!(e.to_string(), "message 7 timed out after 30s");
    }

    #[test]
    fn insufficient_fee_message() {
        let e = XenithError::InsufficientFee {
            required: "500000".into(),
            provided: "100000".into(),
        };
        assert_eq!(
            e.to_string(),
            "insufficient fee: required 500000, provided 100000"
        );
    }

    #[test]
    fn unsupported_chain_message() {
        let e = XenithError::UnsupportedChain(ChainId(9999));
        assert_eq!(
            e.to_string(),
            "chain 9999 is not supported by this transport"
        );
    }

    #[test]
    fn store_error_message() {
        let e = XenithError::StoreError("disk full".into());
        assert_eq!(e.to_string(), "state store error: disk full");
    }

    #[test]
    fn serialization_message() {
        let e = XenithError::Serialization("unexpected EOF".into());
        assert_eq!(e.to_string(), "serialization error: unexpected EOF");
    }

    #[test]
    fn implements_std_error() {
        fn takes_error(_: &dyn std::error::Error) {}
        takes_error(&XenithError::StoreError("x".into()));
    }
}