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)]
9pub enum RetryMethod {
11 ExponentialBackoff {
14 initial_sleep: std::time::Duration,
16 factor: u8,
18 },
19 Fixed {
21 sleep: std::time::Duration,
23 },
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct RPCEndpoint {
29 pub url: url::Url,
31 pub bearer_header: Option<String>,
33 pub retries: u8,
35 pub retry_method: RetryMethod,
37}
38
39impl RPCEndpoint {
40 pub const fn new(url: url::Url) -> Self {
45 Self {
46 url,
47 bearer_header: None,
48 retries: 5,
49 retry_method: RetryMethod::ExponentialBackoff {
51 initial_sleep: std::time::Duration::from_millis(10),
52 factor: 2,
53 },
54 }
55 }
56
57 pub fn mainnet() -> Self {
59 Self::new("https://free.rpc.fastnear.com".parse().unwrap())
60 }
61
62 pub fn mainnet_archival() -> Self {
64 Self::new("https://archival-rpc.mainnet.fastnear.com".parse().unwrap())
65 }
66
67 pub fn testnet() -> Self {
69 Self::new("https://test.rpc.fastnear.com".parse().unwrap())
70 }
71
72 pub fn testnet_archival() -> Self {
74 Self::new("https://archival-rpc.testnet.fastnear.com".parse().unwrap())
75 }
76
77 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 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)]
134pub struct NetworkConfig {
155 pub network_name: String,
157 pub rpc_endpoints: Vec<RPCEndpoint>,
159 pub linkdrop_account_id: Option<AccountId>,
161 pub near_social_db_contract_account_id: Option<AccountId>,
163 pub faucet_url: Option<url::Url>,
165 pub meta_transaction_relayer_url: Option<url::Url>,
167 pub fastnear_url: Option<url::Url>,
171 pub staking_pools_factory_account_id: Option<AccountId>,
173}
174
175impl NetworkConfig {
176 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 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 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 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)]
247pub enum RetryResponse<R, E> {
249 Ok(R),
251 Retry(E),
253 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
266pub 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}