cctp_rs/protocol/
finality.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4//! CCTP v2 finality threshold types
5//!
6//! Circle's CCTP v2 introduces finality thresholds to enable Fast Transfers.
7//! Messages can specify a minimum finality requirement, determining how quickly
8//! attestations are issued.
9//!
10//! Reference: <https://developers.circle.com/cctp/technical-guide>
11
12use std::fmt;
13
14/// Finality threshold for CCTP v2 messages
15///
16/// Determines the level of finality required before Circle's attestation service
17/// will sign a message. Lower thresholds enable faster transfers but may have
18/// slightly higher fees.
19///
20/// # Examples
21///
22/// ```rust
23/// use cctp_rs::FinalityThreshold;
24///
25/// let fast = FinalityThreshold::Fast;
26/// assert_eq!(fast.as_u32(), 1000);
27/// assert_eq!(fast.name(), "Fast Transfer");
28///
29/// let standard = FinalityThreshold::Standard;
30/// assert_eq!(standard.as_u32(), 2000);
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[repr(u32)]
34pub enum FinalityThreshold {
35    /// Fast Transfer - Attestation at confirmed block level (threshold: 1000)
36    ///
37    /// - Settlement time: Under 30 seconds
38    /// - Fee: 0-14 basis points (chain-dependent)
39    /// - Use case: Time-sensitive operations, arbitrage, real-time DeFi
40    Fast = 1000,
41
42    /// Standard Transfer - Attestation at finalized block level (threshold: 2000)
43    ///
44    /// - Settlement time: 13-19 minutes (same as v1)
45    /// - Fee: 0 basis points
46    /// - Use case: Non-urgent transfers, maximum security
47    Standard = 2000,
48}
49
50impl FinalityThreshold {
51    /// Returns the numeric threshold value
52    ///
53    /// # Example
54    ///
55    /// ```rust
56    /// use cctp_rs::FinalityThreshold;
57    ///
58    /// assert_eq!(FinalityThreshold::Fast.as_u32(), 1000);
59    /// assert_eq!(FinalityThreshold::Standard.as_u32(), 2000);
60    /// ```
61    #[inline]
62    pub const fn as_u32(self) -> u32 {
63        self as u32
64    }
65
66    /// Attempts to create a FinalityThreshold from a u32 value
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// use cctp_rs::FinalityThreshold;
72    ///
73    /// assert_eq!(
74    ///     FinalityThreshold::from_u32(1000),
75    ///     Some(FinalityThreshold::Fast)
76    /// );
77    /// assert_eq!(
78    ///     FinalityThreshold::from_u32(2000),
79    ///     Some(FinalityThreshold::Standard)
80    /// );
81    /// assert_eq!(FinalityThreshold::from_u32(1500), None);
82    /// ```
83    #[inline]
84    pub const fn from_u32(value: u32) -> Option<Self> {
85        match value {
86            1000 => Some(Self::Fast),
87            2000 => Some(Self::Standard),
88            _ => None,
89        }
90    }
91
92    /// Returns a descriptive name for this threshold
93    ///
94    /// # Example
95    ///
96    /// ```rust
97    /// use cctp_rs::FinalityThreshold;
98    ///
99    /// assert_eq!(FinalityThreshold::Fast.name(), "Fast Transfer");
100    /// assert_eq!(FinalityThreshold::Standard.name(), "Standard Transfer");
101    /// ```
102    #[inline]
103    pub const fn name(self) -> &'static str {
104        match self {
105            Self::Fast => "Fast Transfer",
106            Self::Standard => "Standard Transfer",
107        }
108    }
109
110    /// Returns true if this is a Fast Transfer threshold
111    ///
112    /// # Example
113    ///
114    /// ```rust
115    /// use cctp_rs::FinalityThreshold;
116    ///
117    /// assert!(FinalityThreshold::Fast.is_fast());
118    /// assert!(!FinalityThreshold::Standard.is_fast());
119    /// ```
120    #[inline]
121    pub const fn is_fast(self) -> bool {
122        matches!(self, Self::Fast)
123    }
124
125    /// Returns true if this is a Standard Transfer threshold
126    ///
127    /// # Example
128    ///
129    /// ```rust
130    /// use cctp_rs::FinalityThreshold;
131    ///
132    /// assert!(FinalityThreshold::Standard.is_standard());
133    /// assert!(!FinalityThreshold::Fast.is_standard());
134    /// ```
135    #[inline]
136    pub const fn is_standard(self) -> bool {
137        matches!(self, Self::Standard)
138    }
139}
140
141impl Default for FinalityThreshold {
142    /// Returns Standard as the default threshold
143    ///
144    /// Standard transfers have no fees and are the safest option.
145    fn default() -> Self {
146        Self::Standard
147    }
148}
149
150impl From<FinalityThreshold> for u32 {
151    #[inline]
152    fn from(threshold: FinalityThreshold) -> Self {
153        threshold.as_u32()
154    }
155}
156
157impl TryFrom<u32> for FinalityThreshold {
158    type Error = InvalidFinalityThreshold;
159
160    #[inline]
161    fn try_from(value: u32) -> Result<Self, Self::Error> {
162        Self::from_u32(value).ok_or(InvalidFinalityThreshold(value))
163    }
164}
165
166impl fmt::Display for FinalityThreshold {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{} ({})", self.name(), self.as_u32())
169    }
170}
171
172/// Error returned when attempting to convert an invalid u32 to a FinalityThreshold
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct InvalidFinalityThreshold(pub u32);
175
176impl fmt::Display for InvalidFinalityThreshold {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(
179            f,
180            "invalid finality threshold: {} (expected 1000 or 2000)",
181            self.0
182        )
183    }
184}
185
186impl std::error::Error for InvalidFinalityThreshold {}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_threshold_values() {
194        assert_eq!(FinalityThreshold::Fast.as_u32(), 1000);
195        assert_eq!(FinalityThreshold::Standard.as_u32(), 2000);
196    }
197
198    #[test]
199    fn test_from_u32_valid() {
200        assert_eq!(
201            FinalityThreshold::from_u32(1000),
202            Some(FinalityThreshold::Fast)
203        );
204        assert_eq!(
205            FinalityThreshold::from_u32(2000),
206            Some(FinalityThreshold::Standard)
207        );
208    }
209
210    #[test]
211    fn test_from_u32_invalid() {
212        assert_eq!(FinalityThreshold::from_u32(0), None);
213        assert_eq!(FinalityThreshold::from_u32(500), None);
214        assert_eq!(FinalityThreshold::from_u32(1500), None);
215        assert_eq!(FinalityThreshold::from_u32(3000), None);
216    }
217
218    #[test]
219    fn test_try_from_valid() {
220        assert_eq!(
221            FinalityThreshold::try_from(1000).unwrap(),
222            FinalityThreshold::Fast
223        );
224        assert_eq!(
225            FinalityThreshold::try_from(2000).unwrap(),
226            FinalityThreshold::Standard
227        );
228    }
229
230    #[test]
231    fn test_try_from_invalid() {
232        assert!(FinalityThreshold::try_from(1500).is_err());
233        let err = FinalityThreshold::try_from(1500).unwrap_err();
234        assert_eq!(err, InvalidFinalityThreshold(1500));
235    }
236
237    #[test]
238    fn test_display() {
239        assert_eq!(
240            format!("{}", FinalityThreshold::Fast),
241            "Fast Transfer (1000)"
242        );
243        assert_eq!(
244            format!("{}", FinalityThreshold::Standard),
245            "Standard Transfer (2000)"
246        );
247    }
248
249    #[test]
250    fn test_name() {
251        assert_eq!(FinalityThreshold::Fast.name(), "Fast Transfer");
252        assert_eq!(FinalityThreshold::Standard.name(), "Standard Transfer");
253    }
254
255    #[test]
256    fn test_is_fast() {
257        assert!(FinalityThreshold::Fast.is_fast());
258        assert!(!FinalityThreshold::Standard.is_fast());
259    }
260
261    #[test]
262    fn test_is_standard() {
263        assert!(FinalityThreshold::Standard.is_standard());
264        assert!(!FinalityThreshold::Fast.is_standard());
265    }
266
267    #[test]
268    fn test_default() {
269        assert_eq!(FinalityThreshold::default(), FinalityThreshold::Standard);
270    }
271
272    #[test]
273    fn test_conversion_roundtrip() {
274        for threshold in [FinalityThreshold::Fast, FinalityThreshold::Standard] {
275            let value: u32 = threshold.into();
276            let parsed = FinalityThreshold::try_from(value).unwrap();
277            assert_eq!(threshold, parsed);
278        }
279    }
280}