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}