use near_api_types::AccountId;
use near_openapi_client::Client;
use reqwest::header::{HeaderValue, InvalidHeaderValue};
use url::Url;
use crate::errors::RetryError;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RetryMethod {
ExponentialBackoff {
initial_sleep: std::time::Duration,
factor: u8,
},
Fixed {
sleep: std::time::Duration,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RPCEndpoint {
pub url: url::Url,
pub bearer_header: Option<String>,
pub retries: u8,
pub retry_method: RetryMethod,
}
impl RPCEndpoint {
pub const fn new(url: url::Url) -> Self {
Self {
url,
bearer_header: None,
retries: 5,
retry_method: RetryMethod::ExponentialBackoff {
initial_sleep: std::time::Duration::from_millis(10),
factor: 2,
},
}
}
pub fn mainnet() -> Self {
Self::new("https://free.rpc.fastnear.com".parse().unwrap())
}
pub fn mainnet_archival() -> Self {
Self::new("https://archival-rpc.mainnet.fastnear.com".parse().unwrap())
}
pub fn testnet() -> Self {
Self::new("https://test.rpc.fastnear.com".parse().unwrap())
}
pub fn testnet_archival() -> Self {
Self::new("https://archival-rpc.testnet.fastnear.com".parse().unwrap())
}
pub fn with_api_key(mut self, api_key: String) -> Self {
self.bearer_header = Some(format!("Bearer {api_key}"));
self
}
pub const fn with_retries(mut self, retries: u8) -> Self {
self.retries = retries;
self
}
pub const fn with_retry_method(mut self, retry_method: RetryMethod) -> Self {
self.retry_method = retry_method;
self
}
pub fn get_sleep_duration(&self, retry: usize) -> std::time::Duration {
match self.retry_method {
RetryMethod::ExponentialBackoff {
initial_sleep,
factor,
} => initial_sleep * ((factor as u32).pow(retry as u32)),
RetryMethod::Fixed { sleep } => sleep,
}
}
pub(crate) fn client(&self) -> Result<Client, InvalidHeaderValue> {
let dur = std::time::Duration::from_secs(15);
let mut client = reqwest::ClientBuilder::new()
.connect_timeout(dur)
.timeout(dur);
if let Some(rpc_api_key) = &self.bearer_header {
let mut headers = reqwest::header::HeaderMap::new();
let mut header = HeaderValue::from_str(rpc_api_key)?;
header.set_sensitive(true);
headers.insert(
reqwest::header::HeaderName::from_static("authorization"),
header.clone(),
);
headers.insert(
reqwest::header::HeaderName::from_static("x-api-key"),
header,
);
client = client.default_headers(headers);
};
Ok(near_openapi_client::Client::new_with_client(
self.url.as_ref().trim_end_matches('/'),
client.build().unwrap(),
))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NetworkConfig {
pub network_name: String,
pub rpc_endpoints: Vec<RPCEndpoint>,
pub linkdrop_account_id: Option<AccountId>,
pub near_social_db_contract_account_id: Option<AccountId>,
pub faucet_url: Option<url::Url>,
pub meta_transaction_relayer_url: Option<url::Url>,
pub fastnear_url: Option<url::Url>,
pub staking_pools_factory_account_id: Option<AccountId>,
}
impl NetworkConfig {
pub fn mainnet() -> Self {
Self {
network_name: "mainnet".to_string(),
rpc_endpoints: vec![RPCEndpoint::mainnet()],
linkdrop_account_id: Some("near".parse().unwrap()),
near_social_db_contract_account_id: Some("social.near".parse().unwrap()),
faucet_url: None,
meta_transaction_relayer_url: None,
fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()),
staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()),
}
}
pub fn mainnet_archival() -> Self {
Self {
network_name: "mainnet-archival".to_string(),
rpc_endpoints: vec![RPCEndpoint::mainnet_archival()],
linkdrop_account_id: Some("near".parse().unwrap()),
near_social_db_contract_account_id: Some("social.near".parse().unwrap()),
faucet_url: None,
meta_transaction_relayer_url: None,
fastnear_url: Some("https://api.fastnear.com/".parse().unwrap()),
staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()),
}
}
pub fn testnet() -> Self {
Self {
network_name: "testnet".to_string(),
rpc_endpoints: vec![RPCEndpoint::testnet()],
linkdrop_account_id: Some("testnet".parse().unwrap()),
near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()),
faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()),
meta_transaction_relayer_url: None,
fastnear_url: None,
staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()),
}
}
pub fn testnet_archival() -> Self {
Self {
network_name: "testnet-archival".to_string(),
rpc_endpoints: vec![RPCEndpoint::testnet_archival()],
linkdrop_account_id: Some("testnet".parse().unwrap()),
near_social_db_contract_account_id: Some("v1.social08.testnet".parse().unwrap()),
faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()),
meta_transaction_relayer_url: None,
fastnear_url: None,
staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()),
}
}
pub fn from_rpc_url(name: &str, rpc_url: Url) -> Self {
Self {
network_name: name.to_string(),
rpc_endpoints: vec![RPCEndpoint::new(rpc_url)],
linkdrop_account_id: None,
near_social_db_contract_account_id: None,
faucet_url: None,
fastnear_url: None,
meta_transaction_relayer_url: None,
staking_pools_factory_account_id: None,
}
}
}
#[derive(Debug)]
pub enum RetryResponse<R, E> {
Ok(R),
Retry(E),
Critical(E),
}
impl<R, E> From<Result<R, E>> for RetryResponse<R, E> {
fn from(value: Result<R, E>) -> Self {
match value {
Ok(value) => Self::Ok(value),
Err(value) => Self::Retry(value),
}
}
}
pub async fn retry<R, E, T, F>(network: NetworkConfig, mut task: F) -> Result<R, RetryError<E>>
where
F: FnMut(Client) -> T + Send,
T: core::future::Future<Output = RetryResponse<R, E>> + Send,
T::Output: Send,
E: Send,
{
if network.rpc_endpoints.is_empty() {
return Err(RetryError::NoRpcEndpoints);
}
let mut last_error = None;
for endpoint in network.rpc_endpoints.iter() {
let client = endpoint
.client()
.map_err(|e| RetryError::InvalidApiKey(e))?;
for retry in 0..endpoint.retries {
let result = task(client.clone()).await;
match result {
RetryResponse::Ok(result) => return Ok(result),
RetryResponse::Retry(error) => {
last_error = Some(error);
tokio::time::sleep(endpoint.get_sleep_duration(retry as usize)).await;
}
RetryResponse::Critical(result) => return Err(RetryError::Critical(result)),
}
}
}
Err(RetryError::RetriesExhausted(last_error.expect(
"Logic error: last_error should be Some when all retries are exhausted",
)))
}