Skip to main content

tendermint_rpc/client/
compat.rs

1//! Support for dynamic compatibility with older protocol versions.
2
3use core::fmt;
4use core::str::FromStr;
5
6use serde::{de::Deserializer, Deserialize, Serialize, Serializer};
7
8use tendermint::Version;
9
10use crate::prelude::*;
11use crate::Error;
12
13/// Protocol compatibility mode for a Tendermint RPC client.
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub enum CompatMode {
16    /// Use version 0.34 of the protocol.
17    V0_34,
18    /// Use version 0.37 of the protocol.
19    V0_37,
20    /// Use version 0.38 of the protocol.
21    V0_38,
22    // NOTE: When adding a newer version, do not forget to update:
23    // - CompatMode::latest()
24    // - CompatMode::from_version()
25    // - impl Display for CompatMode
26    // - impl FromStr for CompatMode
27    // - The tests
28}
29
30impl Default for CompatMode {
31    fn default() -> Self {
32        CompatMode::latest()
33    }
34}
35
36impl CompatMode {
37    /// The latest supported version, selected by default.
38    pub const fn latest() -> Self {
39        Self::V0_38
40    }
41
42    /// Parse the Tendermint version string to determine
43    /// the compatibility mode.
44    ///
45    /// The version can be obtained by querying the `/status` endpoint.
46    /// The request and response format of this endpoint is currently the same
47    /// for all supported RPC dialects, so such a request can be performed
48    /// before the required compatibility mode is settled upon.
49    ///
50    /// Note that this is not fail-proof: the version is reported for a particular
51    /// client connection, so any other connections to the same URL might not
52    /// be handled by the same server. In the future, the RPC protocol should
53    /// follow versioning practices designed to avoid ambiguities with
54    /// message formats.
55    pub fn from_version(tendermint_version: Version) -> Result<CompatMode, Error> {
56        let raw_version: String = tendermint_version.into();
57        let version = semver::Version::parse(raw_version.trim_start_matches('v'))
58            .map_err(|_| Error::invalid_tendermint_version(raw_version))?;
59
60        match (version.major, version.minor) {
61            (0, 34) => Ok(CompatMode::V0_34),
62            (0, 37) => Ok(CompatMode::V0_37),
63            (0, 38) => Ok(CompatMode::V0_38),
64            _ => Err(Error::unsupported_tendermint_version(version.to_string())),
65        }
66    }
67}
68
69impl fmt::Display for CompatMode {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            CompatMode::V0_34 => f.write_str("v0.34"),
73            CompatMode::V0_37 => f.write_str("v0.37"),
74            CompatMode::V0_38 => f.write_str("v0.38"),
75        }
76    }
77}
78
79impl FromStr for CompatMode {
80    type Err = Error;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        const VALID_COMPAT_MODES: &str = "v0.34, v0.37, v0.38";
84
85        // Trim leading 'v', if present
86        match s.trim_start_matches('v') {
87            "0.34" => Ok(CompatMode::V0_34),
88            "0.37" => Ok(CompatMode::V0_37),
89            "0.38" => Ok(CompatMode::V0_38),
90            _ => Err(Error::invalid_compat_mode(
91                s.to_string(),
92                VALID_COMPAT_MODES,
93            )),
94        }
95    }
96}
97
98impl<'de> Deserialize<'de> for CompatMode {
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        use serde::de;
104
105        let s = String::deserialize(deserializer)?;
106        FromStr::from_str(&s).map_err(de::Error::custom)
107    }
108}
109
110impl Serialize for CompatMode {
111    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112    where
113        S: Serializer,
114    {
115        self.to_string().serialize(serializer)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::CompatMode;
122    use crate::prelude::*;
123    use tendermint::Version;
124
125    fn parse_version(s: &str) -> Version {
126        let json = format!("\"{s}\"");
127        serde_json::from_str(&json).unwrap()
128    }
129
130    #[test]
131    fn test_parse_version_for_compat_mode() {
132        assert_eq!(
133            CompatMode::from_version(parse_version("v0.34.16")).unwrap(),
134            CompatMode::V0_34
135        );
136        assert_eq!(
137            CompatMode::from_version(parse_version("v0.37.0-pre1")).unwrap(),
138            CompatMode::V0_37
139        );
140        assert_eq!(
141            CompatMode::from_version(parse_version("v0.37.0")).unwrap(),
142            CompatMode::V0_37
143        );
144        assert_eq!(
145            CompatMode::from_version(parse_version("v0.38.0")).unwrap(),
146            CompatMode::V0_38
147        );
148        let res = CompatMode::from_version(parse_version("v0.39.0"));
149        assert!(res.is_err());
150        let res = CompatMode::from_version(parse_version("v1.0.0"));
151        assert!(res.is_err());
152        let res = CompatMode::from_version(parse_version("poobah"));
153        assert!(res.is_err());
154    }
155
156    #[test]
157    fn test_from_str() {
158        assert_eq!("0.34".parse::<CompatMode>().unwrap(), CompatMode::V0_34);
159        assert_eq!("0.37".parse::<CompatMode>().unwrap(), CompatMode::V0_37);
160        assert_eq!("0.38".parse::<CompatMode>().unwrap(), CompatMode::V0_38);
161
162        let res = "0.33".parse::<CompatMode>();
163        assert!(res.is_err());
164        let res = "0.39".parse::<CompatMode>();
165        assert!(res.is_err());
166        let res = "foobar".parse::<CompatMode>();
167        assert!(res.is_err());
168    }
169}