Skip to main content

chaincodec_core/
chain.rs

1//! Chain family and identifier types.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Top-level blockchain VM family.
7/// Determines which decoder is dispatched for a raw event.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum ChainFamily {
11    Evm,
12    Solana,
13    Cosmos,
14    Sui,
15    Aptos,
16    /// Third-party or experimental chains registered via the plugin system.
17    Custom(String),
18}
19
20impl fmt::Display for ChainFamily {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            ChainFamily::Evm => write!(f, "evm"),
24            ChainFamily::Solana => write!(f, "solana"),
25            ChainFamily::Cosmos => write!(f, "cosmos"),
26            ChainFamily::Sui => write!(f, "sui"),
27            ChainFamily::Aptos => write!(f, "aptos"),
28            ChainFamily::Custom(s) => write!(f, "{s}"),
29        }
30    }
31}
32
33/// A fully qualified chain identifier, e.g. `ethereum`, `arbitrum`, `solana-mainnet`.
34/// Used as the primary key when selecting a decoder and querying schemas.
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct ChainId {
37    /// Human-readable slug, e.g. "ethereum", "arbitrum-one", "solana-mainnet"
38    pub slug: String,
39    /// EVM chain ID integer, if applicable (e.g. 1 for Ethereum mainnet)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub evm_chain_id: Option<u64>,
42    /// The VM family this chain belongs to
43    pub family: ChainFamily,
44}
45
46impl ChainId {
47    pub fn evm(slug: impl Into<String>, chain_id: u64) -> Self {
48        Self {
49            slug: slug.into(),
50            evm_chain_id: Some(chain_id),
51            family: ChainFamily::Evm,
52        }
53    }
54
55    pub fn solana(slug: impl Into<String>) -> Self {
56        Self {
57            slug: slug.into(),
58            evm_chain_id: None,
59            family: ChainFamily::Solana,
60        }
61    }
62
63    pub fn cosmos(slug: impl Into<String>) -> Self {
64        Self {
65            slug: slug.into(),
66            evm_chain_id: None,
67            family: ChainFamily::Cosmos,
68        }
69    }
70
71    pub fn custom(slug: impl Into<String>, family_name: impl Into<String>) -> Self {
72        let slug = slug.into();
73        Self {
74            slug,
75            evm_chain_id: None,
76            family: ChainFamily::Custom(family_name.into()),
77        }
78    }
79}
80
81impl fmt::Display for ChainId {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(f, "{}", self.slug)
84    }
85}
86
87/// Well-known chain IDs for convenience.
88pub mod chains {
89    use super::ChainId;
90
91    pub fn ethereum() -> ChainId { ChainId::evm("ethereum", 1) }
92    pub fn arbitrum() -> ChainId { ChainId::evm("arbitrum", 42161) }
93    pub fn base() -> ChainId { ChainId::evm("base", 8453) }
94    pub fn polygon() -> ChainId { ChainId::evm("polygon", 137) }
95    pub fn optimism() -> ChainId { ChainId::evm("optimism", 10) }
96    pub fn solana_mainnet() -> ChainId { ChainId::solana("solana-mainnet") }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn chain_id_display() {
105        assert_eq!(chains::ethereum().to_string(), "ethereum");
106        assert_eq!(chains::arbitrum().to_string(), "arbitrum");
107    }
108
109    #[test]
110    fn chain_family_serde() {
111        let json = serde_json::to_string(&ChainFamily::Evm).unwrap();
112        assert_eq!(json, "\"evm\"");
113        let parsed: ChainFamily = serde_json::from_str(&json).unwrap();
114        assert_eq!(parsed, ChainFamily::Evm);
115    }
116}