1pub mod runtime_transport;
4
5use crate::{
6 ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT, provider::runtime_transport::RuntimeTransportBuilder,
7};
8use alloy_provider::{
9 Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
10 fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
11 network::{AnyNetwork, EthereumWallet},
12};
13use alloy_rpc_client::ClientBuilder;
14use alloy_transport::{layers::RetryBackoffLayer, utils::guess_local_url};
15use eyre::{Result, WrapErr};
16use foundry_config::NamedChain;
17use reqwest::Url;
18use std::{
19 net::SocketAddr,
20 path::{Path, PathBuf},
21 str::FromStr,
22 time::Duration,
23};
24use url::ParseError;
25
26const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
29
30const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
32
33pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
35
36pub type RetryProviderWithSigner<N = AnyNetwork> = FillProvider<
38 JoinFill<
39 JoinFill<
40 Identity,
41 JoinFill<
42 GasFiller,
43 JoinFill<
44 alloy_provider::fillers::BlobGasFiller,
45 JoinFill<NonceFiller, ChainIdFiller>,
46 >,
47 >,
48 >,
49 WalletFiller<EthereumWallet>,
50 >,
51 RootProvider<N>,
52 N,
53>;
54
55#[inline]
72#[track_caller]
73pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
74 try_get_http_provider(builder).unwrap()
75}
76
77#[inline]
80pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
81 ProviderBuilder::new(builder.as_ref()).build()
82}
83
84#[derive(Debug)]
86pub struct ProviderBuilder {
87 url: Result<Url>,
89 chain: NamedChain,
90 max_retry: u32,
91 initial_backoff: u64,
92 timeout: Duration,
93 compute_units_per_second: u64,
95 jwt: Option<String>,
97 headers: Vec<String>,
98 is_local: bool,
99 accept_invalid_certs: bool,
101}
102
103impl ProviderBuilder {
104 pub fn new(url_str: &str) -> Self {
106 let mut url_str = url_str;
108
109 let storage;
112 if url_str.starts_with("localhost:") {
113 storage = format!("http://{url_str}");
114 url_str = storage.as_str();
115 }
116
117 let url = Url::parse(url_str)
118 .or_else(|err| match err {
119 ParseError::RelativeUrlWithoutBase => {
120 if SocketAddr::from_str(url_str).is_ok() {
121 Url::parse(&format!("http://{url_str}"))
122 } else {
123 let path = Path::new(url_str);
124
125 if let Ok(path) = resolve_path(path) {
126 Url::parse(&format!("file://{}", path.display()))
127 } else {
128 Err(err)
129 }
130 }
131 }
132 _ => Err(err),
133 })
134 .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
135
136 let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
138
139 Self {
140 url,
141 chain: NamedChain::Mainnet,
142 max_retry: 8,
143 initial_backoff: 800,
144 timeout: REQUEST_TIMEOUT,
145 compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
147 jwt: None,
148 headers: vec![],
149 is_local,
150 accept_invalid_certs: false,
151 }
152 }
153
154 pub fn timeout(mut self, timeout: Duration) -> Self {
161 self.timeout = timeout;
162 self
163 }
164
165 pub fn chain(mut self, chain: NamedChain) -> Self {
167 self.chain = chain;
168 self
169 }
170
171 pub fn max_retry(mut self, max_retry: u32) -> Self {
173 self.max_retry = max_retry;
174 self
175 }
176
177 pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
179 self.max_retry = max_retry.unwrap_or(self.max_retry);
180 self
181 }
182
183 pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
186 self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
187 self
188 }
189
190 pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
192 self.initial_backoff = initial_backoff;
193 self
194 }
195
196 pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
200 self.compute_units_per_second = compute_units_per_second;
201 self
202 }
203
204 pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
208 if let Some(cups) = compute_units_per_second {
209 self.compute_units_per_second = cups;
210 }
211 self
212 }
213
214 pub fn local(mut self, is_local: bool) -> Self {
218 self.is_local = is_local;
219 self
220 }
221
222 pub fn aggressive(self) -> Self {
226 self.max_retry(100).initial_backoff(100).local(true)
227 }
228
229 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
231 self.jwt = Some(jwt.into());
232 self
233 }
234
235 pub fn headers(mut self, headers: Vec<String>) -> Self {
237 self.headers = headers;
238
239 self
240 }
241
242 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
244 self.headers = headers.unwrap_or(self.headers);
245 self
246 }
247
248 pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
250 self.accept_invalid_certs = accept_invalid_certs;
251 self
252 }
253
254 pub fn build(self) -> Result<RetryProvider> {
256 let Self {
257 url,
258 chain,
259 max_retry,
260 initial_backoff,
261 timeout,
262 compute_units_per_second,
263 jwt,
264 headers,
265 is_local,
266 accept_invalid_certs,
267 } = self;
268 let url = url?;
269
270 let retry_layer =
271 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
272
273 let transport = RuntimeTransportBuilder::new(url)
274 .with_timeout(timeout)
275 .with_headers(headers)
276 .with_jwt(jwt)
277 .accept_invalid_certs(accept_invalid_certs)
278 .build();
279 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
280
281 if !is_local {
282 client.set_poll_interval(
283 chain
284 .average_blocktime_hint()
285 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
288 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
289 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
290 );
291 }
292
293 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
294 .connect_provider(RootProvider::new(client));
295
296 Ok(provider)
297 }
298
299 pub fn build_with_wallet(self, wallet: EthereumWallet) -> Result<RetryProviderWithSigner> {
301 let Self {
302 url,
303 chain,
304 max_retry,
305 initial_backoff,
306 timeout,
307 compute_units_per_second,
308 jwt,
309 headers,
310 is_local,
311 accept_invalid_certs,
312 } = self;
313 let url = url?;
314
315 let retry_layer =
316 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
317
318 let transport = RuntimeTransportBuilder::new(url)
319 .with_timeout(timeout)
320 .with_headers(headers)
321 .with_jwt(jwt)
322 .accept_invalid_certs(accept_invalid_certs)
323 .build();
324
325 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
326
327 if !is_local {
328 client.set_poll_interval(
329 chain
330 .average_blocktime_hint()
331 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
332 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
333 );
334 }
335
336 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
337 .with_recommended_fillers()
338 .wallet(wallet)
339 .connect_provider(RootProvider::new(client));
340
341 Ok(provider)
342 }
343}
344
345#[cfg(not(windows))]
346fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
347 if path.is_absolute() {
348 Ok(path.to_path_buf())
349 } else {
350 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
351 }
352}
353
354#[cfg(windows)]
355fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
356 if let Some(s) = path.to_str() {
357 if s.starts_with(r"\\.\pipe\") {
358 return Ok(path.to_path_buf());
359 }
360 }
361 Err(())
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn can_auto_correct_missing_prefix() {
370 let builder = ProviderBuilder::new("localhost:8545");
371 assert!(builder.url.is_ok());
372
373 let url = builder.url.unwrap();
374 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
375 }
376}