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 testnet() -> Self {
64 Self::new("https://test.rpc.fastnear.com".parse().unwrap())
65 }
66
67 pub fn with_api_key(mut self, api_key: String) -> Self {
69 self.bearer_header = Some(format!("Bearer {api_key}"));
70 self
71 }
72
73 pub const fn with_retries(mut self, retries: u8) -> Self {
75 self.retries = retries;
76 self
77 }
78
79 pub const fn with_retry_method(mut self, retry_method: RetryMethod) -> Self {
80 self.retry_method = retry_method;
81 self
82 }
83
84 pub fn get_sleep_duration(&self, retry: usize) -> std::time::Duration {
85 match self.retry_method {
86 RetryMethod::ExponentialBackoff {
87 initial_sleep,
88 factor,
89 } => initial_sleep * ((factor as u32).pow(retry as u32)),
90 RetryMethod::Fixed { sleep } => sleep,
91 }
92 }
93}
94
95#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
96pub struct NetworkConfig {
117 pub network_name: String,
119 pub rpc_endpoints: Vec<RPCEndpoint>,
121 pub linkdrop_account_id: Option<AccountId>,
123 pub near_social_db_contract_account_id: Option<AccountId>,
125 pub faucet_url: Option<url::Url>,
127 pub meta_transaction_relayer_url: Option<url::Url>,
129 pub fastnear_url: Option<url::Url>,
133 pub staking_pools_factory_account_id: Option<AccountId>,
135}
136
137impl NetworkConfig {
138 pub fn mainnet() -> Self {
140 Self {
141 network_name: "mainnet".to_string(),
142 rpc_endpoints: vec![RPCEndpoint::mainnet()],
143 linkdrop_account_id: Some("near".parse().unwrap()),
144 near_social_db_contract_account_id: Some("social.near".parse().unwrap()),
145 faucet_url: None,
146 meta_transaction_relayer_url: None,
147 fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()),
148 staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()),
149 }
150 }
151
152 pub fn testnet() -> Self {
154 Self {
155 network_name: "testnet".to_string(),
156 rpc_endpoints: vec![RPCEndpoint::testnet()],
157 linkdrop_account_id: Some("testnet".parse().unwrap()),
158 near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()),
159 faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()),
160 meta_transaction_relayer_url: None,
161 fastnear_url: None,
162 staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()),
163 }
164 }
165
166 pub fn from_rpc_url(name: &str, rpc_url: Url) -> Self {
167 Self {
168 network_name: name.to_string(),
169 rpc_endpoints: vec![RPCEndpoint::new(rpc_url)],
170 linkdrop_account_id: None,
171 near_social_db_contract_account_id: None,
172 faucet_url: None,
173 fastnear_url: None,
174 meta_transaction_relayer_url: None,
175 staking_pools_factory_account_id: None,
176 }
177 }
178
179 pub(crate) fn client(&self, index: usize) -> Result<Client, InvalidHeaderValue> {
180 let rpc_endpoint = &self.rpc_endpoints[index];
181
182 let dur = std::time::Duration::from_secs(15);
183 let mut client = reqwest::ClientBuilder::new()
184 .connect_timeout(dur)
185 .timeout(dur);
186
187 if let Some(rpc_api_key) = &rpc_endpoint.bearer_header {
188 let mut headers = reqwest::header::HeaderMap::new();
189
190 let mut header = HeaderValue::from_str(rpc_api_key)?;
191 header.set_sensitive(true);
192
193 headers.insert(
194 reqwest::header::HeaderName::from_static("x-api-key"),
195 header,
196 );
197 client = client.default_headers(headers);
198 };
199 Ok(near_openapi_client::Client::new_with_client(
200 rpc_endpoint.url.as_ref().trim_end_matches('/'),
201 client.build().unwrap(),
202 ))
203 }
204}
205
206#[derive(Debug)]
207pub enum RetryResponse<R, E> {
209 Ok(R),
211 Retry(E),
213 Critical(E),
215}
216
217impl<R, E> From<Result<R, E>> for RetryResponse<R, E> {
218 fn from(value: Result<R, E>) -> Self {
219 match value {
220 Ok(value) => Self::Ok(value),
221 Err(value) => Self::Retry(value),
222 }
223 }
224}
225
226pub async fn retry<R, E, T, F>(network: NetworkConfig, mut task: F) -> Result<R, RetryError<E>>
232where
233 F: FnMut(Client) -> T + Send,
234 T: core::future::Future<Output = RetryResponse<R, E>> + Send,
235 T::Output: Send,
236 E: Send,
237{
238 if network.rpc_endpoints.is_empty() {
239 return Err(RetryError::NoRpcEndpoints);
240 }
241
242 let mut last_error = None;
243 for (index, endpoint) in network.rpc_endpoints.iter().enumerate() {
244 let client = network
245 .client(index)
246 .map_err(|e| RetryError::InvalidApiKey(e))?;
247 for retry in 0..endpoint.retries {
248 let result = task(client.clone()).await;
249 match result {
250 RetryResponse::Ok(result) => return Ok(result),
251 RetryResponse::Retry(error) => {
252 last_error = Some(error);
253 tokio::time::sleep(endpoint.get_sleep_duration(retry as usize)).await;
254 }
255 RetryResponse::Critical(result) => return Err(RetryError::Critical(result)),
256 }
257 }
258 }
259 Err(RetryError::RetriesExhausted(last_error.expect(
260 "Logic error: last_error should be Some when all retries are exhausted",
261 )))
262}