Skip to main content

near_api/
config.rs

1use near_api_types::AccountId;
2use near_openapi_client::Client;
3use reqwest::header::{HeaderValue, InvalidHeaderValue};
4use url::Url;
5
6use crate::errors::RetryError;
7
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9/// Specifies the retry strategy for RPC endpoint requests.
10pub enum RetryMethod {
11    /// Exponential backoff strategy with configurable initial delay and multiplication factor.
12    /// The delay is calculated as: `initial_sleep * factor^retry_number`
13    ExponentialBackoff {
14        /// The initial delay duration before the first retry
15        initial_sleep: std::time::Duration,
16        /// The multiplication factor for calculating subsequent delays
17        factor: u8,
18    },
19    /// Fixed delay strategy with constant sleep duration
20    Fixed {
21        /// The constant delay duration between retries
22        sleep: std::time::Duration,
23    },
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27/// Configuration for a [NEAR RPC](https://docs.near.org/api/rpc/providers) endpoint with retry and backoff settings.
28pub struct RPCEndpoint {
29    /// The URL of the RPC endpoint
30    pub url: url::Url,
31    /// Optional API key for authenticated requests
32    pub bearer_header: Option<String>,
33    /// Number of consecutive failures to move on to the next endpoint.
34    pub retries: u8,
35    /// The retry method to use
36    pub retry_method: RetryMethod,
37}
38
39impl RPCEndpoint {
40    /// Constructs a new RPC endpoint configuration with default settings.
41    ///
42    /// The default retry method is `ExponentialBackoff` with an initial sleep of 10ms and a factor of 2.
43    /// The delays will be 10ms, 20ms, 40ms, 80ms, 160ms.
44    pub const fn new(url: url::Url) -> Self {
45        Self {
46            url,
47            bearer_header: None,
48            retries: 5,
49            // 10ms, 20ms, 40ms, 80ms, 160ms
50            retry_method: RetryMethod::ExponentialBackoff {
51                initial_sleep: std::time::Duration::from_millis(10),
52                factor: 2,
53            },
54        }
55    }
56
57    /// Constructs default mainnet configuration.
58    pub fn mainnet() -> Self {
59        Self::new("https://free.rpc.fastnear.com".parse().unwrap())
60    }
61
62    /// Constructs default mainnet archival configuration.
63    pub fn mainnet_archival() -> Self {
64        Self::new("https://archival-rpc.mainnet.fastnear.com".parse().unwrap())
65    }
66
67    /// Constructs default testnet configuration.
68    pub fn testnet() -> Self {
69        Self::new("https://test.rpc.fastnear.com".parse().unwrap())
70    }
71
72    /// Constructs default testnet archival configuration.
73    pub fn testnet_archival() -> Self {
74        Self::new("https://archival-rpc.testnet.fastnear.com".parse().unwrap())
75    }
76
77    /// Set API key for the endpoint.
78    pub fn with_api_key(mut self, api_key: String) -> Self {
79        self.bearer_header = Some(format!("Bearer {api_key}"));
80        self
81    }
82
83    /// Set number of retries for the endpoint before moving on to the next one.
84    pub const fn with_retries(mut self, retries: u8) -> Self {
85        self.retries = retries;
86        self
87    }
88
89    pub const fn with_retry_method(mut self, retry_method: RetryMethod) -> Self {
90        self.retry_method = retry_method;
91        self
92    }
93
94    pub fn get_sleep_duration(&self, retry: usize) -> std::time::Duration {
95        match self.retry_method {
96            RetryMethod::ExponentialBackoff {
97                initial_sleep,
98                factor,
99            } => initial_sleep * ((factor as u32).pow(retry as u32)),
100            RetryMethod::Fixed { sleep } => sleep,
101        }
102    }
103
104    pub(crate) fn client(&self) -> Result<Client, InvalidHeaderValue> {
105        let dur = std::time::Duration::from_secs(15);
106        let mut client = reqwest::ClientBuilder::new()
107            .connect_timeout(dur)
108            .timeout(dur);
109
110        if let Some(rpc_api_key) = &self.bearer_header {
111            let mut headers = reqwest::header::HeaderMap::new();
112
113            let mut header = HeaderValue::from_str(rpc_api_key)?;
114            header.set_sensitive(true);
115
116            headers.insert(
117                reqwest::header::HeaderName::from_static("authorization"),
118                header.clone(),
119            );
120            headers.insert(
121                reqwest::header::HeaderName::from_static("x-api-key"),
122                header,
123            );
124            client = client.default_headers(headers);
125        };
126        Ok(near_openapi_client::Client::new_with_client(
127            self.url.as_ref().trim_end_matches('/'),
128            client.build().unwrap(),
129        ))
130    }
131}
132
133#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
134/// Configuration for a NEAR network including RPC endpoints and network-specific settings.
135///
136/// # Multiple RPC endpoints
137///
138/// This struct is used to configure multiple RPC endpoints for a NEAR network.
139/// It allows for failover between endpoints in case of a failure.
140///
141///
142/// ## Example
143/// ```rust,no_run
144/// use near_api::*;
145///
146/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
147/// let config = NetworkConfig {
148///     rpc_endpoints: vec![RPCEndpoint::mainnet(), RPCEndpoint::new("https://near.lava.build".parse()?)],
149///     ..NetworkConfig::mainnet()
150/// };
151/// # Ok(())
152/// # }
153/// ```
154pub struct NetworkConfig {
155    /// Human readable name of the network (e.g. "mainnet", "testnet")
156    pub network_name: String,
157    /// List of [RPC endpoints](https://docs.near.org/api/rpc/providers) to use with failover
158    pub rpc_endpoints: Vec<RPCEndpoint>,
159    /// Account ID used for [linkdrop functionality](https://docs.near.org/build/primitives/linkdrop)
160    pub linkdrop_account_id: Option<AccountId>,
161    /// Account ID of the [NEAR Social contract](https://docs.near.org/social/contract)
162    pub near_social_db_contract_account_id: Option<AccountId>,
163    /// URL of the network's faucet service
164    pub faucet_url: Option<url::Url>,
165    /// URL for the [meta transaction relayer](https://docs.near.org/concepts/abstraction/relayers) service
166    pub meta_transaction_relayer_url: Option<url::Url>,
167    /// URL for the [fastnear](https://docs.near.org/tools/ecosystem-apis/fastnear-api) service.
168    ///
169    /// Currently, unused. See [#30](https://github.com/near/near-api-rs/issues/30)
170    pub fastnear_url: Option<url::Url>,
171    /// Account ID of the [staking pools factory](https://github.com/NearSocial/social-db)
172    pub staking_pools_factory_account_id: Option<AccountId>,
173}
174
175impl NetworkConfig {
176    /// Constructs default mainnet configuration.
177    pub fn mainnet() -> Self {
178        Self {
179            network_name: "mainnet".to_string(),
180            rpc_endpoints: vec![RPCEndpoint::mainnet()],
181            linkdrop_account_id: Some("near".parse().unwrap()),
182            near_social_db_contract_account_id: Some("social.near".parse().unwrap()),
183            faucet_url: None,
184            meta_transaction_relayer_url: None,
185            fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()),
186            staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()),
187        }
188    }
189
190    /// Constructs default mainnet archival configuration.
191    pub fn mainnet_archival() -> Self {
192        Self {
193            network_name: "mainnet-archival".to_string(),
194            rpc_endpoints: vec![RPCEndpoint::mainnet_archival()],
195            linkdrop_account_id: Some("near".parse().unwrap()),
196            near_social_db_contract_account_id: Some("social.near".parse().unwrap()),
197            faucet_url: None,
198            meta_transaction_relayer_url: None,
199            fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()),
200            staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()),
201        }
202    }
203
204    /// Constructs default testnet configuration.
205    pub fn testnet() -> Self {
206        Self {
207            network_name: "testnet".to_string(),
208            rpc_endpoints: vec![RPCEndpoint::testnet()],
209            linkdrop_account_id: Some("testnet".parse().unwrap()),
210            near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()),
211            faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()),
212            meta_transaction_relayer_url: None,
213            fastnear_url: None,
214            staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()),
215        }
216    }
217
218    /// Constructs default testnet archival configuration.
219    pub fn testnet_archival() -> Self {
220        Self {
221            network_name: "testnet-archival".to_string(),
222            rpc_endpoints: vec![RPCEndpoint::testnet_archival()],
223            linkdrop_account_id: Some("testnet".parse().unwrap()),
224            near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()),
225            faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()),
226            meta_transaction_relayer_url: None,
227            fastnear_url: None,
228            staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()),
229        }
230    }
231
232    pub fn from_rpc_url(name: &str, rpc_url: Url) -> Self {
233        Self {
234            network_name: name.to_string(),
235            rpc_endpoints: vec![RPCEndpoint::new(rpc_url)],
236            linkdrop_account_id: None,
237            near_social_db_contract_account_id: None,
238            faucet_url: None,
239            fastnear_url: None,
240            meta_transaction_relayer_url: None,
241            staking_pools_factory_account_id: None,
242        }
243    }
244}
245
246#[derive(Debug)]
247/// Represents the possible outcomes of a retry-able operation.
248pub enum RetryResponse<R, E> {
249    /// Operation succeeded with result R
250    Ok(R),
251    /// Operation failed with error E, should be retried
252    Retry(E),
253    /// Operation failed with critical error E, should not be retried
254    Critical(E),
255}
256
257impl<R, E> From<Result<R, E>> for RetryResponse<R, E> {
258    fn from(value: Result<R, E>) -> Self {
259        match value {
260            Ok(value) => Self::Ok(value),
261            Err(value) => Self::Retry(value),
262        }
263    }
264}
265
266/// Retry a task with exponential backoff and failover.
267///
268/// # Arguments
269/// * `network` - The network configuration to use for the retry-able operation.
270/// * `task` - The task to retry.
271pub async fn retry<R, E, T, F>(network: NetworkConfig, mut task: F) -> Result<R, RetryError<E>>
272where
273    F: FnMut(Client) -> T + Send,
274    T: core::future::Future<Output = RetryResponse<R, E>> + Send,
275    T::Output: Send,
276    E: Send,
277{
278    if network.rpc_endpoints.is_empty() {
279        return Err(RetryError::NoRpcEndpoints);
280    }
281
282    let mut last_error = None;
283    for endpoint in network.rpc_endpoints.iter() {
284        let client = endpoint
285            .client()
286            .map_err(|e| RetryError::InvalidApiKey(e))?;
287        for retry in 0..endpoint.retries {
288            let result = task(client.clone()).await;
289            match result {
290                RetryResponse::Ok(result) => return Ok(result),
291                RetryResponse::Retry(error) => {
292                    last_error = Some(error);
293                    tokio::time::sleep(endpoint.get_sleep_duration(retry as usize)).await;
294                }
295                RetryResponse::Critical(result) => return Err(RetryError::Critical(result)),
296            }
297        }
298    }
299    Err(RetryError::RetriesExhausted(last_error.expect(
300        "Logic error: last_error should be Some when all retries are exhausted",
301    )))
302}