cctp_rs/protocol/
domain_id.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4//! CCTP domain ID types for identifying blockchain networks
5//!
6//! Circle's Cross-Chain Transfer Protocol uses domain IDs as unique identifiers
7//! for each supported blockchain network. This module provides a strongly-typed
8//! enum to prevent invalid domain IDs at compile time.
9//!
10//! Reference: <https://developers.circle.com/stablecoins/evm-smart-contracts>
11
12use std::fmt;
13
14/// CCTP domain identifier for blockchain networks
15///
16/// Each blockchain network supported by Circle's CCTP has a unique domain ID.
17/// This enum provides type-safe representation of these identifiers.
18///
19/// # CCTP Version Support
20///
21/// - Domains 0-10: Supported in CCTP v1 and v2
22/// - Domains 11+: Only supported in CCTP v2
23///
24/// # Example
25///
26/// ```rust
27/// use cctp_rs::DomainId;
28///
29/// let ethereum_domain = DomainId::Ethereum;
30/// let domain_value: u32 = ethereum_domain.into();
31/// assert_eq!(domain_value, 0);
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[repr(u32)]
35#[non_exhaustive]
36pub enum DomainId {
37    /// Ethereum mainnet and Sepolia testnet (Domain ID: 0)
38    Ethereum = 0,
39    /// Avalanche C-Chain (Domain ID: 1)
40    Avalanche = 1,
41    /// Optimism (Domain ID: 2)
42    Optimism = 2,
43    /// Arbitrum One and Arbitrum Sepolia (Domain ID: 3)
44    Arbitrum = 3,
45    /// Solana (Domain ID: 5) - Non-EVM chain, v2 only
46    Solana = 5,
47    /// Base and Base Sepolia (Domain ID: 6)
48    Base = 6,
49    /// Polygon PoS (Domain ID: 7)
50    Polygon = 7,
51    /// Unichain (Domain ID: 10)
52    Unichain = 10,
53    /// Linea (Domain ID: 11) - v2 only
54    Linea = 11,
55    /// Codex (Domain ID: 12) - v2 only
56    Codex = 12,
57    /// Sonic (Domain ID: 13) - v2 only
58    Sonic = 13,
59    /// World Chain (Domain ID: 14) - v2 only
60    WorldChain = 14,
61    /// Monad (Domain ID: 15) - v2 only
62    Monad = 15,
63    /// Sei (Domain ID: 16) - v2 only
64    Sei = 16,
65    /// BNB Smart Chain (Domain ID: 17) - v2 only
66    BnbSmartChain = 17,
67    /// XDC Network (Domain ID: 18) - v2 only
68    Xdc = 18,
69    /// HyperEVM (Domain ID: 19) - v2 only
70    HyperEvm = 19,
71    /// Ink (Domain ID: 21) - v2 only
72    Ink = 21,
73    /// Plume (Domain ID: 22) - v2 only
74    Plume = 22,
75    /// Starknet Testnet (Domain ID: 25) - Non-EVM chain, v2 only
76    StarknetTestnet = 25,
77    /// Arc Testnet (Domain ID: 26) - v2 only
78    ArcTestnet = 26,
79}
80
81impl DomainId {
82    /// Returns the numeric domain ID value
83    ///
84    /// # Example
85    ///
86    /// ```rust
87    /// use cctp_rs::DomainId;
88    ///
89    /// assert_eq!(DomainId::Ethereum.as_u32(), 0);
90    /// assert_eq!(DomainId::Arbitrum.as_u32(), 3);
91    /// ```
92    #[inline]
93    pub const fn as_u32(self) -> u32 {
94        self as u32
95    }
96
97    /// Attempts to create a DomainId from a u32 value
98    ///
99    /// # Example
100    ///
101    /// ```rust
102    /// use cctp_rs::DomainId;
103    ///
104    /// assert_eq!(DomainId::from_u32(0), Some(DomainId::Ethereum));
105    /// assert_eq!(DomainId::from_u32(3), Some(DomainId::Arbitrum));
106    /// assert_eq!(DomainId::from_u32(11), Some(DomainId::Linea));
107    /// assert_eq!(DomainId::from_u32(999), None);
108    /// ```
109    #[inline]
110    pub const fn from_u32(value: u32) -> Option<Self> {
111        match value {
112            0 => Some(Self::Ethereum),
113            1 => Some(Self::Avalanche),
114            2 => Some(Self::Optimism),
115            3 => Some(Self::Arbitrum),
116            5 => Some(Self::Solana),
117            6 => Some(Self::Base),
118            7 => Some(Self::Polygon),
119            10 => Some(Self::Unichain),
120            11 => Some(Self::Linea),
121            12 => Some(Self::Codex),
122            13 => Some(Self::Sonic),
123            14 => Some(Self::WorldChain),
124            15 => Some(Self::Monad),
125            16 => Some(Self::Sei),
126            17 => Some(Self::BnbSmartChain),
127            18 => Some(Self::Xdc),
128            19 => Some(Self::HyperEvm),
129            21 => Some(Self::Ink),
130            22 => Some(Self::Plume),
131            25 => Some(Self::StarknetTestnet),
132            26 => Some(Self::ArcTestnet),
133            _ => None,
134        }
135    }
136
137    /// Returns the chain name as a string
138    ///
139    /// # Example
140    ///
141    /// ```rust
142    /// use cctp_rs::DomainId;
143    ///
144    /// assert_eq!(DomainId::Ethereum.name(), "Ethereum");
145    /// assert_eq!(DomainId::Arbitrum.name(), "Arbitrum");
146    /// assert_eq!(DomainId::Linea.name(), "Linea");
147    /// ```
148    #[inline]
149    pub const fn name(self) -> &'static str {
150        match self {
151            Self::Ethereum => "Ethereum",
152            Self::Avalanche => "Avalanche",
153            Self::Optimism => "Optimism",
154            Self::Arbitrum => "Arbitrum",
155            Self::Solana => "Solana",
156            Self::Base => "Base",
157            Self::Polygon => "Polygon",
158            Self::Unichain => "Unichain",
159            Self::Linea => "Linea",
160            Self::Codex => "Codex",
161            Self::Sonic => "Sonic",
162            Self::WorldChain => "World Chain",
163            Self::Monad => "Monad",
164            Self::Sei => "Sei",
165            Self::BnbSmartChain => "BNB Smart Chain",
166            Self::Xdc => "XDC",
167            Self::HyperEvm => "HyperEVM",
168            Self::Ink => "Ink",
169            Self::Plume => "Plume",
170            Self::StarknetTestnet => "Starknet Testnet",
171            Self::ArcTestnet => "Arc Testnet",
172        }
173    }
174}
175
176impl From<DomainId> for u32 {
177    #[inline]
178    fn from(domain: DomainId) -> Self {
179        domain.as_u32()
180    }
181}
182
183impl TryFrom<u32> for DomainId {
184    type Error = InvalidDomainId;
185
186    #[inline]
187    fn try_from(value: u32) -> Result<Self, Self::Error> {
188        Self::from_u32(value).ok_or(InvalidDomainId(value))
189    }
190}
191
192impl fmt::Display for DomainId {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "{} ({})", self.name(), self.as_u32())
195    }
196}
197
198/// Error returned when attempting to convert an invalid u32 to a DomainId
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct InvalidDomainId(pub u32);
201
202impl fmt::Display for InvalidDomainId {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "invalid CCTP domain ID: {}", self.0)
205    }
206}
207
208impl std::error::Error for InvalidDomainId {}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_domain_id_values() {
216        // v1 and v2 chains
217        assert_eq!(DomainId::Ethereum.as_u32(), 0);
218        assert_eq!(DomainId::Avalanche.as_u32(), 1);
219        assert_eq!(DomainId::Optimism.as_u32(), 2);
220        assert_eq!(DomainId::Arbitrum.as_u32(), 3);
221        assert_eq!(DomainId::Base.as_u32(), 6);
222        assert_eq!(DomainId::Polygon.as_u32(), 7);
223        assert_eq!(DomainId::Unichain.as_u32(), 10);
224
225        // v2 only chains
226        assert_eq!(DomainId::Solana.as_u32(), 5);
227        assert_eq!(DomainId::Linea.as_u32(), 11);
228        assert_eq!(DomainId::Codex.as_u32(), 12);
229        assert_eq!(DomainId::Sonic.as_u32(), 13);
230        assert_eq!(DomainId::WorldChain.as_u32(), 14);
231        assert_eq!(DomainId::Monad.as_u32(), 15);
232        assert_eq!(DomainId::Sei.as_u32(), 16);
233        assert_eq!(DomainId::BnbSmartChain.as_u32(), 17);
234        assert_eq!(DomainId::Xdc.as_u32(), 18);
235        assert_eq!(DomainId::HyperEvm.as_u32(), 19);
236        assert_eq!(DomainId::Ink.as_u32(), 21);
237        assert_eq!(DomainId::Plume.as_u32(), 22);
238        assert_eq!(DomainId::StarknetTestnet.as_u32(), 25);
239        assert_eq!(DomainId::ArcTestnet.as_u32(), 26);
240    }
241
242    #[test]
243    fn test_from_u32_valid() {
244        // v1 and v2 chains
245        assert_eq!(DomainId::from_u32(0), Some(DomainId::Ethereum));
246        assert_eq!(DomainId::from_u32(1), Some(DomainId::Avalanche));
247        assert_eq!(DomainId::from_u32(2), Some(DomainId::Optimism));
248        assert_eq!(DomainId::from_u32(3), Some(DomainId::Arbitrum));
249        assert_eq!(DomainId::from_u32(6), Some(DomainId::Base));
250        assert_eq!(DomainId::from_u32(7), Some(DomainId::Polygon));
251        assert_eq!(DomainId::from_u32(10), Some(DomainId::Unichain));
252
253        // v2 only chains - priority chains
254        assert_eq!(DomainId::from_u32(11), Some(DomainId::Linea));
255        assert_eq!(DomainId::from_u32(13), Some(DomainId::Sonic));
256        assert_eq!(DomainId::from_u32(16), Some(DomainId::Sei));
257        assert_eq!(DomainId::from_u32(17), Some(DomainId::BnbSmartChain));
258
259        // v2 only chains - other
260        assert_eq!(DomainId::from_u32(5), Some(DomainId::Solana));
261        assert_eq!(DomainId::from_u32(12), Some(DomainId::Codex));
262        assert_eq!(DomainId::from_u32(14), Some(DomainId::WorldChain));
263        assert_eq!(DomainId::from_u32(15), Some(DomainId::Monad));
264        assert_eq!(DomainId::from_u32(18), Some(DomainId::Xdc));
265        assert_eq!(DomainId::from_u32(19), Some(DomainId::HyperEvm));
266        assert_eq!(DomainId::from_u32(21), Some(DomainId::Ink));
267        assert_eq!(DomainId::from_u32(22), Some(DomainId::Plume));
268        assert_eq!(DomainId::from_u32(25), Some(DomainId::StarknetTestnet));
269        assert_eq!(DomainId::from_u32(26), Some(DomainId::ArcTestnet));
270    }
271
272    #[test]
273    fn test_from_u32_invalid() {
274        // Test gaps in domain ID space
275        assert_eq!(DomainId::from_u32(4), None); // Gap
276        assert_eq!(DomainId::from_u32(8), None); // Gap
277        assert_eq!(DomainId::from_u32(9), None); // Gap
278        assert_eq!(DomainId::from_u32(20), None); // Gap
279        assert_eq!(DomainId::from_u32(23), None); // Gap
280        assert_eq!(DomainId::from_u32(24), None); // Gap
281        assert_eq!(DomainId::from_u32(27), None); // Beyond current
282        assert_eq!(DomainId::from_u32(999), None); // Way beyond
283    }
284
285    #[test]
286    fn test_try_from_valid() {
287        assert_eq!(DomainId::try_from(0).unwrap(), DomainId::Ethereum);
288        assert_eq!(DomainId::try_from(3).unwrap(), DomainId::Arbitrum);
289    }
290
291    #[test]
292    fn test_try_from_invalid() {
293        assert!(DomainId::try_from(999).is_err());
294        let err = DomainId::try_from(999).unwrap_err();
295        assert_eq!(err, InvalidDomainId(999));
296    }
297
298    #[test]
299    fn test_display() {
300        assert_eq!(format!("{}", DomainId::Ethereum), "Ethereum (0)");
301        assert_eq!(format!("{}", DomainId::Arbitrum), "Arbitrum (3)");
302        assert_eq!(format!("{}", DomainId::Base), "Base (6)");
303    }
304
305    #[test]
306    fn test_name() {
307        assert_eq!(DomainId::Ethereum.name(), "Ethereum");
308        assert_eq!(DomainId::Arbitrum.name(), "Arbitrum");
309        assert_eq!(DomainId::Avalanche.name(), "Avalanche");
310    }
311
312    #[test]
313    fn test_conversion_roundtrip() {
314        for domain in [
315            // v1 and v2 chains
316            DomainId::Ethereum,
317            DomainId::Avalanche,
318            DomainId::Optimism,
319            DomainId::Arbitrum,
320            DomainId::Base,
321            DomainId::Polygon,
322            DomainId::Unichain,
323            // v2 only chains
324            DomainId::Solana,
325            DomainId::Linea,
326            DomainId::Codex,
327            DomainId::Sonic,
328            DomainId::WorldChain,
329            DomainId::Monad,
330            DomainId::Sei,
331            DomainId::BnbSmartChain,
332            DomainId::Xdc,
333            DomainId::HyperEvm,
334            DomainId::Ink,
335            DomainId::Plume,
336            DomainId::StarknetTestnet,
337            DomainId::ArcTestnet,
338        ] {
339            let value: u32 = domain.into();
340            let parsed = DomainId::try_from(value).unwrap();
341            assert_eq!(domain, parsed);
342        }
343    }
344}