Skip to main content

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