cometbft-rpc 0.1.0-alpha.2

cometbft-rpc contains the core types returned by a CometBFT node's RPC endpoint. All networking related features are feature guarded to keep the dependencies small in cases where only the core types are needed.
Documentation
//! Support for dynamic compatibility with older protocol versions.

use core::fmt;

use cometbft::Version;

use crate::prelude::*;
use crate::Error;

/// Protocol compatibility mode for a CometBFT RPC client.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CompatMode {
    /// Use a compatibility mode for the RPC protocol used in CometBFT 0.34.
    V0_34,
    /// Use a compatibility mode for the RPC protocol used since CometBFT 0.37.
    /// This is the default mode that has persisted into CometBFT 1.0.
    V0_37,
}

impl Default for CompatMode {
    fn default() -> Self {
        CompatMode::latest()
    }
}

impl CompatMode {
    /// The latest supported version, selected by default.
    pub const fn latest() -> Self {
        Self::V0_37
    }

    /// Parse the CometBFT version string to determine
    /// the compatibility mode.
    ///
    /// The version can be obtained by querying the `/status` endpoint.
    /// The request and response format of this endpoint is currently the same
    /// for all supported RPC dialects, so such a request can be performed
    /// before the required compatibility mode is settled upon.
    ///
    /// Note that this is not fail-proof: the version is reported for a particular
    /// client connection, so any other connections to the same URL might not
    /// be handled by the same server. In the future, the RPC protocol should
    /// follow versioning practices designed to avoid ambiguities with
    /// message formats.
    pub fn from_version(cometbft_version: Version) -> Result<CompatMode, Error> {
        let raw_version: String = cometbft_version.into();
        let version = semver::Version::parse(raw_version.trim_start_matches('v'))
            .map_err(|_| Error::invalid_cometbft_version(raw_version))?;

        match (version.major, version.minor) {
            (1, _) => Ok(CompatMode::V0_37),
            (0, 34) => Ok(CompatMode::V0_34),
            (0, 37) => Ok(CompatMode::V0_37),
            (0, 38) => Ok(CompatMode::V0_37),
            _ => Err(Error::unsupported_cometbft_version(version.to_string())),
        }
    }
}

impl fmt::Display for CompatMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CompatMode::V0_34 => f.write_str("v0.34"),
            CompatMode::V0_37 => f.write_str("v0.37"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::CompatMode;
    use crate::prelude::*;
    use cometbft::Version;

    fn parse_version(s: &str) -> Version {
        let json = format!("\"{s}\"");
        serde_json::from_str(&json).unwrap()
    }

    #[test]
    fn test_parse_version_for_compat_mode() {
        assert_eq!(
            CompatMode::from_version(parse_version("v0.34.16")).unwrap(),
            CompatMode::V0_34
        );
        assert_eq!(
            CompatMode::from_version(parse_version("v0.37.0-pre1")).unwrap(),
            CompatMode::V0_37
        );
        assert_eq!(
            CompatMode::from_version(parse_version("v0.37.0")).unwrap(),
            CompatMode::V0_37
        );
        assert_eq!(
            CompatMode::from_version(parse_version("v0.38.0")).unwrap(),
            CompatMode::V0_37
        );
        let res = CompatMode::from_version(parse_version("v0.39.0"));
        assert!(res.is_err());
        assert_eq!(
            CompatMode::from_version(parse_version("v1.0.0-alpha.1")).unwrap(),
            CompatMode::V0_37
        );
        assert_eq!(
            CompatMode::from_version(parse_version("v1.0.0")).unwrap(),
            CompatMode::V0_37
        );
        assert_eq!(
            CompatMode::from_version(parse_version("v1.1.0")).unwrap(),
            CompatMode::V0_37
        );
        let res = CompatMode::from_version(parse_version("poobah"));
        assert!(res.is_err());
    }
}