cctp_rs/
provider.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Provider utilities for CCTP operations.
6//!
7//! This module provides helpers for gas estimation and provider configuration
8//! to improve reliability of cross-chain transfers.
9//!
10//! # Production Provider Configuration
11//!
12//! For production deployments, configure your providers with retry logic and timeouts.
13//! Alloy supports Tower layers for middleware-style request handling.
14//!
15//! ## Using Throttle for Rate Limiting
16//!
17//! Enable the `throttle` feature on `alloy-provider` and use the throttle layer:
18//!
19//! ```rust,ignore
20//! use alloy_provider::ProviderBuilder;
21//! use std::time::Duration;
22//!
23//! // Provider with 10 requests/second rate limit
24//! let provider = ProviderBuilder::new()
25//!     .throttle(10)  // 10 RPS limit
26//!     .connect_http("https://eth.llamarpc.com".parse()?)
27//!     .await?;
28//! ```
29//!
30//! ## Custom Retry Logic with Error Detection
31//!
32//! Use the typed error detection methods for implementing retry logic:
33//!
34//! ```rust,ignore
35//! use cctp_rs::{CctpError, ProviderConfig};
36//! use std::time::Duration;
37//! use tokio::time::sleep;
38//!
39//! async fn with_retry<T, F, Fut>(config: &ProviderConfig, mut f: F) -> Result<T, CctpError>
40//! where
41//!     F: FnMut() -> Fut,
42//!     Fut: std::future::Future<Output = Result<T, CctpError>>,
43//! {
44//!     let mut attempts = 0;
45//!     loop {
46//!         attempts += 1;
47//!         match f().await {
48//!             Ok(result) => return Ok(result),
49//!             Err(e) if e.is_transient() && attempts < config.retry_attempts => {
50//!                 // Transient error - retry with exponential backoff
51//!                 let backoff = Duration::from_millis(200 * 2u64.pow(attempts - 1));
52//!                 sleep(backoff.min(config.timeout)).await;
53//!                 continue;
54//!             }
55//!             Err(e) if e.is_rate_limited() && attempts < config.retry_attempts => {
56//!                 // Rate limited - wait longer before retry
57//!                 sleep(Duration::from_secs(5)).await;
58//!                 continue;
59//!             }
60//!             Err(e) => return Err(e),
61//!         }
62//!     }
63//! }
64//!
65//! // Usage:
66//! let config = ProviderConfig::high_reliability();
67//! let result = with_retry(&config, || async {
68//!     bridge.get_attestation(tx_hash, polling_config).await
69//! }).await?;
70//! ```
71//!
72//! ## Recommended Configurations
73//!
74//! | Use Case | Configuration | Description |
75//! |----------|--------------|-------------|
76//! | Fast transfers | `ProviderConfig::fast_transfer()` | 5 retries, 15s timeout |
77//! | Reliable batch ops | `ProviderConfig::high_reliability()` | 10 retries, 60s timeout |
78//! | Public endpoints | `ProviderConfig::rate_limited(5)` | 3 retries, 30s timeout, 5 RPS |
79//! | Default | `ProviderConfig::default()` | 3 retries, 30s timeout |
80
81use crate::error::{CctpError, Result};
82use alloy_network::Ethereum;
83use alloy_primitives::U256;
84use alloy_provider::Provider;
85use alloy_rpc_types::TransactionRequest;
86use std::time::Duration;
87
88/// Default gas buffer percentage (20%)
89pub const DEFAULT_GAS_BUFFER_PERCENT: u64 = 20;
90
91/// Default request timeout in seconds
92pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
93
94/// Default number of retry attempts
95pub const DEFAULT_RETRY_ATTEMPTS: u32 = 3;
96
97/// Estimate gas for a transaction with an optional safety buffer.
98///
99/// This helper calls the provider's `estimate_gas` method and adds a configurable
100/// percentage buffer to prevent out-of-gas failures on complex transfers like
101/// CCTP burns and mints.
102///
103/// # Arguments
104///
105/// * `provider` - The Ethereum provider to use for estimation
106/// * `tx` - The transaction request to estimate gas for
107/// * `buffer_percent` - Optional percentage buffer to add (defaults to 20%)
108///
109/// # Returns
110///
111/// The estimated gas limit with the buffer applied.
112///
113/// # Example
114///
115/// ```rust,ignore
116/// use cctp_rs::provider::estimate_gas_with_buffer;
117///
118/// let gas_limit = estimate_gas_with_buffer(&provider, &tx, Some(20)).await?;
119/// let tx = tx.with_gas_limit(gas_limit);
120/// ```
121pub async fn estimate_gas_with_buffer<P: Provider<Ethereum>>(
122    provider: &P,
123    tx: &TransactionRequest,
124    buffer_percent: Option<u64>,
125) -> Result<u64> {
126    let buffer = buffer_percent.unwrap_or(DEFAULT_GAS_BUFFER_PERCENT);
127
128    let estimate = provider
129        .estimate_gas(tx.clone())
130        .await
131        .map_err(|e| CctpError::Provider(format!("Gas estimation failed: {e}")))?;
132
133    // Apply buffer: estimate * (100 + buffer) / 100
134    let with_buffer = estimate.saturating_mul(100 + buffer) / 100;
135
136    Ok(with_buffer)
137}
138
139/// Configuration for creating production-ready providers.
140///
141/// This struct encapsulates recommended settings for CCTP operations,
142/// including retry behavior and timeouts.
143///
144/// # Example
145///
146/// ```rust
147/// use cctp_rs::ProviderConfig;
148/// use std::time::Duration;
149///
150/// // Use defaults
151/// let config = ProviderConfig::default();
152///
153/// // Or customize
154/// let config = ProviderConfig::builder()
155///     .retry_attempts(5)
156///     .timeout(Duration::from_secs(60))
157///     .build();
158/// ```
159#[derive(Debug, Clone)]
160pub struct ProviderConfig {
161    /// Number of retry attempts for failed requests
162    pub retry_attempts: u32,
163    /// Request timeout duration
164    pub timeout: Duration,
165    /// Optional rate limit (requests per second)
166    pub rate_limit_rps: Option<u32>,
167}
168
169impl Default for ProviderConfig {
170    fn default() -> Self {
171        Self {
172            retry_attempts: DEFAULT_RETRY_ATTEMPTS,
173            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
174            rate_limit_rps: None,
175        }
176    }
177}
178
179impl ProviderConfig {
180    /// Creates a new builder for ProviderConfig
181    pub fn builder() -> ProviderConfigBuilder {
182        ProviderConfigBuilder::default()
183    }
184
185    /// Creates a configuration optimized for fast transfers
186    ///
187    /// Uses shorter timeouts and more aggressive retry settings
188    /// suitable for time-sensitive fast transfer operations.
189    pub fn fast_transfer() -> Self {
190        Self {
191            retry_attempts: 5,
192            timeout: Duration::from_secs(15),
193            rate_limit_rps: None,
194        }
195    }
196
197    /// Creates a configuration for high-reliability operations
198    ///
199    /// Uses longer timeouts and more retry attempts for
200    /// operations where reliability is more important than speed.
201    pub fn high_reliability() -> Self {
202        Self {
203            retry_attempts: 10,
204            timeout: Duration::from_secs(60),
205            rate_limit_rps: None,
206        }
207    }
208
209    /// Creates a configuration for rate-limited public endpoints
210    ///
211    /// Includes rate limiting to avoid hitting provider limits
212    /// on public RPC endpoints.
213    pub fn rate_limited(rps: u32) -> Self {
214        Self {
215            retry_attempts: DEFAULT_RETRY_ATTEMPTS,
216            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
217            rate_limit_rps: Some(rps),
218        }
219    }
220}
221
222/// Builder for [`ProviderConfig`]
223#[derive(Debug, Clone, Default)]
224pub struct ProviderConfigBuilder {
225    retry_attempts: Option<u32>,
226    timeout: Option<Duration>,
227    rate_limit_rps: Option<u32>,
228}
229
230impl ProviderConfigBuilder {
231    /// Sets the number of retry attempts
232    pub fn retry_attempts(mut self, attempts: u32) -> Self {
233        self.retry_attempts = Some(attempts);
234        self
235    }
236
237    /// Sets the request timeout
238    pub fn timeout(mut self, timeout: Duration) -> Self {
239        self.timeout = Some(timeout);
240        self
241    }
242
243    /// Sets the rate limit in requests per second
244    pub fn rate_limit_rps(mut self, rps: u32) -> Self {
245        self.rate_limit_rps = Some(rps);
246        self
247    }
248
249    /// Builds the ProviderConfig
250    pub fn build(self) -> ProviderConfig {
251        ProviderConfig {
252            retry_attempts: self.retry_attempts.unwrap_or(DEFAULT_RETRY_ATTEMPTS),
253            timeout: self
254                .timeout
255                .unwrap_or(Duration::from_secs(DEFAULT_TIMEOUT_SECS)),
256            rate_limit_rps: self.rate_limit_rps,
257        }
258    }
259}
260
261/// Helper to calculate gas price with a tip buffer for EIP-1559 transactions.
262///
263/// This adds a configurable percentage buffer to the max priority fee
264/// to help ensure transactions are included in blocks during congestion.
265///
266/// # Arguments
267///
268/// * `base_fee` - The current base fee from the latest block
269/// * `max_priority_fee` - The desired priority fee (tip)
270/// * `buffer_percent` - Percentage buffer to add to the priority fee
271///
272/// # Returns
273///
274/// A tuple of (max_fee_per_gas, max_priority_fee_per_gas) with buffer applied
275///
276/// # Example
277///
278/// ```rust
279/// use cctp_rs::calculate_gas_price_with_buffer;
280/// use alloy_primitives::U256;
281///
282/// let base_fee = U256::from(30_000_000_000u64); // 30 gwei
283/// let priority_fee = U256::from(2_000_000_000u64); // 2 gwei
284///
285/// let (max_fee, max_priority) = calculate_gas_price_with_buffer(
286///     base_fee,
287///     priority_fee,
288///     20, // 20% buffer
289/// );
290/// ```
291pub fn calculate_gas_price_with_buffer(
292    base_fee: U256,
293    max_priority_fee: U256,
294    buffer_percent: u64,
295) -> (U256, U256) {
296    // Apply buffer to priority fee
297    let buffered_priority = max_priority_fee * U256::from(100 + buffer_percent) / U256::from(100);
298
299    // Max fee = 2 * base_fee + buffered_priority (standard EIP-1559 formula with buffer)
300    let max_fee = base_fee * U256::from(2) + buffered_priority;
301
302    (max_fee, buffered_priority)
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_provider_config_default() {
311        let config = ProviderConfig::default();
312        assert_eq!(config.retry_attempts, 3);
313        assert_eq!(config.timeout, Duration::from_secs(30));
314        assert!(config.rate_limit_rps.is_none());
315    }
316
317    #[test]
318    fn test_provider_config_builder() {
319        let config = ProviderConfig::builder()
320            .retry_attempts(5)
321            .timeout(Duration::from_secs(60))
322            .rate_limit_rps(10)
323            .build();
324
325        assert_eq!(config.retry_attempts, 5);
326        assert_eq!(config.timeout, Duration::from_secs(60));
327        assert_eq!(config.rate_limit_rps, Some(10));
328    }
329
330    #[test]
331    fn test_provider_config_fast_transfer() {
332        let config = ProviderConfig::fast_transfer();
333        assert_eq!(config.retry_attempts, 5);
334        assert_eq!(config.timeout, Duration::from_secs(15));
335    }
336
337    #[test]
338    fn test_provider_config_high_reliability() {
339        let config = ProviderConfig::high_reliability();
340        assert_eq!(config.retry_attempts, 10);
341        assert_eq!(config.timeout, Duration::from_secs(60));
342    }
343
344    #[test]
345    fn test_provider_config_rate_limited() {
346        let config = ProviderConfig::rate_limited(5);
347        assert_eq!(config.rate_limit_rps, Some(5));
348    }
349
350    #[test]
351    fn test_gas_price_with_buffer() {
352        let base_fee = U256::from(30_000_000_000u64); // 30 gwei
353        let priority_fee = U256::from(2_000_000_000u64); // 2 gwei
354
355        let (max_fee, max_priority) = calculate_gas_price_with_buffer(base_fee, priority_fee, 20);
356
357        // Priority should be 2.4 gwei (2 + 20%)
358        assert_eq!(max_priority, U256::from(2_400_000_000u64));
359
360        // Max fee should be 2 * 30 + 2.4 = 62.4 gwei
361        assert_eq!(max_fee, U256::from(62_400_000_000u64));
362    }
363
364    #[test]
365    fn test_gas_price_with_zero_buffer() {
366        let base_fee = U256::from(30_000_000_000u64);
367        let priority_fee = U256::from(2_000_000_000u64);
368
369        let (max_fee, max_priority) = calculate_gas_price_with_buffer(base_fee, priority_fee, 0);
370
371        assert_eq!(max_priority, priority_fee);
372        assert_eq!(max_fee, base_fee * U256::from(2) + priority_fee);
373    }
374}