use crate::{Sdk, SdkInner};
use backon::ExponentialBuilder;
use reqwest::Client;
use std::{
collections::HashSet,
sync::{Arc, LazyLock},
time::Duration,
};
use url::Url;
#[cfg(feature = "eas-verifier")]
use {
alloy_primitives::ChainId,
alloy_provider::{Provider, ProviderBuilder},
std::collections::BTreeMap,
uts_contracts::provider_helper::{RetryBackoffArgs, ThrottleArgs},
};
static DEFAULT_CALENDARS: LazyLock<HashSet<Url>> = LazyLock::new(|| {
HashSet::from([
Url::parse("https://lgm1.calendar.test.timestamps.now/").unwrap(),
Url::parse("https://a.pool.opentimestamps.org/").unwrap(),
Url::parse("https://b.pool.opentimestamps.org/").unwrap(),
Url::parse("https://a.pool.eternitywall.com/").unwrap(),
Url::parse("https://ots.btc.catallaxy.com/").unwrap(),
])
});
#[cfg(feature = "eas-verifier")]
static DEFAULT_PROVIDERS: LazyLock<BTreeMap<ChainId, Url>> = LazyLock::new(|| {
BTreeMap::from([
(1, Url::parse("https://0xrpc.io/eth").unwrap()),
(11155111, Url::parse("https://0xrpc.io/sep").unwrap()),
(534352, Url::parse("https://rpc.scroll.io").unwrap()),
(534351, Url::parse("https://sepolia-rpc.scroll.io").unwrap()),
])
});
#[derive(Debug, thiserror::Error)]
pub enum BuilderError {
#[error("At least one calendar must be specified")]
NoCalendars,
#[error("Quorum of {quorum} is too high for only {calendar_count} calendars")]
QuorumTooHigh {
quorum: usize,
calendar_count: usize,
},
}
type Result<T, E = BuilderError> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct SdkBuilder {
http_client: Option<Client>,
calendars: HashSet<Url>,
quorum: usize,
timeout_seconds: u64,
retry: ExponentialBuilder,
nonce_size: usize,
keep_pending: bool,
#[cfg(feature = "eas-verifier")]
eth_providers: BTreeMap<ChainId, Url>,
#[cfg(feature = "eas-verifier")]
eth_compute_units_per_second: u64,
#[cfg(feature = "eas-verifier")]
eth_requests_per_second: u32,
#[cfg(feature = "bitcoin-verifier")]
bitcoin_rpc: Url,
}
impl Default for SdkBuilder {
fn default() -> Self {
Self::try_default_from_calendars(DEFAULT_CALENDARS.clone())
.expect("Default calendars should be valid")
}
}
impl SdkBuilder {
pub fn empty() -> Self {
Self {
http_client: None,
calendars: HashSet::new(),
quorum: 0,
timeout_seconds: 5,
retry: ExponentialBuilder::default(),
nonce_size: 32,
keep_pending: false,
#[cfg(feature = "eas-verifier")]
eth_providers: BTreeMap::new(),
#[cfg(feature = "eas-verifier")]
eth_compute_units_per_second: 20,
#[cfg(feature = "eas-verifier")]
eth_requests_per_second: 25,
#[cfg(feature = "bitcoin-verifier")]
bitcoin_rpc: Url::parse("https://bitcoin-rpc.publicnode.com").unwrap(),
}
}
pub fn try_default_from_calendars(calendars: impl IntoIterator<Item = Url>) -> Result<Self> {
let calendars = calendars.into_iter().collect::<HashSet<_>>();
if calendars.is_empty() {
return Err(BuilderError::NoCalendars);
}
let this = Self {
calendars,
#[cfg(feature = "eas-verifier")]
eth_providers: DEFAULT_PROVIDERS.clone(),
..Self::empty()
};
Ok(this.with_two_thirds_quorum())
}
pub fn with_http_client(mut self, http_client: Client) -> Self {
self.http_client = Some(http_client);
self
}
pub fn add_calendar(mut self, calendar: Url) -> Self {
self.calendars.insert(calendar);
self
}
pub fn with_quorum(mut self, quorum: usize) -> Self {
self.quorum = quorum.max(1);
self
}
pub fn with_two_thirds_quorum(self) -> Self {
let two_thirds = (self.calendars.len() * 2).div_ceil(3);
self.with_quorum(two_thirds)
}
pub fn with_timeout_seconds(mut self, timeout_seconds: u64) -> Self {
self.timeout_seconds = timeout_seconds;
self
}
pub fn with_jitter(mut self) -> Self {
self.retry = self.retry.with_jitter();
self
}
pub fn with_backoff_factor(mut self, factor: f32) -> Self {
self.retry = self.retry.with_factor(factor);
self
}
pub fn with_min_backoff_delay(mut self, min_delay: Duration) -> Self {
self.retry = self.retry.with_min_delay(min_delay);
self
}
pub fn with_max_backoff_delay(mut self, max_delay: Duration) -> Self {
self.retry = self.retry.with_max_delay(max_delay);
self
}
pub fn with_max_backoff_attempts(mut self, max_times: usize) -> Self {
self.retry = self.retry.with_max_times(max_times);
self
}
pub fn with_max_backoff_total_delay(mut self, total_delay: Option<Duration>) -> Self {
self.retry = self.retry.with_total_delay(total_delay);
self
}
pub fn with_nonce_size(mut self, nonce_size: usize) -> Self {
self.nonce_size = nonce_size;
self
}
pub fn keep_pending(mut self) -> Self {
self.keep_pending = true;
self
}
#[cfg(feature = "eas-verifier")]
pub fn add_eth_provider(mut self, chain_id: ChainId, url: Url) -> Self {
self.eth_providers.insert(chain_id, url);
self
}
#[cfg(feature = "eas-verifier")]
pub fn with_eth_compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
self.eth_compute_units_per_second = compute_units_per_second;
self
}
#[cfg(feature = "eas-verifier")]
pub fn with_eth_requests_per_second(mut self, requests_per_second: u32) -> Self {
self.eth_requests_per_second = requests_per_second;
self
}
pub fn build(self) -> Result<Sdk> {
if self.calendars.is_empty() {
return Err(BuilderError::NoCalendars);
}
if self.quorum > self.calendars.len() {
return Err(BuilderError::QuorumTooHigh {
quorum: self.quorum,
calendar_count: self.calendars.len(),
});
}
let http_client = if let Some(client) = self.http_client {
client
} else {
Client::builder()
.user_agent(concat!("uts/", env!("CARGO_PKG_VERSION")))
.build()
.expect("default HTTP client should be valid")
};
#[cfg(feature = "eas-verifier")]
let eth_providers = {
let eth_retry = RetryBackoffArgs {
compute_units_per_second: self.eth_compute_units_per_second,
..Default::default()
};
let eth_throttle = ThrottleArgs {
requests_per_second: self.eth_requests_per_second,
};
self.eth_providers
.into_iter()
.map(|(chain_id, url)| {
let provider = ProviderBuilder::new().connect_client(
alloy_rpc_client::ClientBuilder::default()
.layer(eth_retry.layer())
.layer(eth_throttle.layer())
.http(url),
);
(chain_id, provider.erased())
})
.collect()
};
Ok(Sdk {
inner: Arc::new(SdkInner {
http_client,
calendars: self.calendars,
timeout_seconds: self.timeout_seconds,
retry: self.retry,
quorum: self.quorum,
nonce_size: self.nonce_size,
keep_pending: self.keep_pending,
#[cfg(feature = "eas-verifier")]
eth_providers,
#[cfg(feature = "bitcoin-verifier")]
bitcoin_rpc: self.bitcoin_rpc,
}),
})
}
}