1pub mod curl_transport;
4pub mod runtime_transport;
5
6use crate::{
7 ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
8 provider::{curl_transport::CurlTransport, runtime_transport::RuntimeTransportBuilder},
9};
10use alloy_chains::NamedChain;
11use alloy_provider::{
12 Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
13 fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
14 network::{AnyNetwork, EthereumWallet},
15};
16use alloy_rpc_client::ClientBuilder;
17use alloy_transport::{layers::RetryBackoffLayer, utils::guess_local_url};
18use eyre::{Result, WrapErr};
19use reqwest::Url;
20use std::{
21 net::SocketAddr,
22 path::{Path, PathBuf},
23 str::FromStr,
24 time::Duration,
25};
26use url::ParseError;
27
28const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
31
32const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
34
35pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
37
38pub type RetryProviderWithSigner<N = AnyNetwork> = FillProvider<
40 JoinFill<
41 JoinFill<
42 Identity,
43 JoinFill<
44 GasFiller,
45 JoinFill<
46 alloy_provider::fillers::BlobGasFiller,
47 JoinFill<NonceFiller, ChainIdFiller>,
48 >,
49 >,
50 >,
51 WalletFiller<EthereumWallet>,
52 >,
53 RootProvider<N>,
54 N,
55>;
56
57#[inline]
74#[track_caller]
75pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
76 try_get_http_provider(builder).unwrap()
77}
78
79#[inline]
82pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
83 ProviderBuilder::new(builder.as_ref()).build()
84}
85
86#[derive(Debug)]
88pub struct ProviderBuilder {
89 url: Result<Url>,
91 chain: NamedChain,
92 max_retry: u32,
93 initial_backoff: u64,
94 timeout: Duration,
95 compute_units_per_second: u64,
97 jwt: Option<String>,
99 headers: Vec<String>,
100 is_local: bool,
101 accept_invalid_certs: bool,
103 no_proxy: bool,
105 curl_mode: bool,
107}
108
109impl ProviderBuilder {
110 pub fn new(url_str: &str) -> Self {
112 let mut url_str = url_str;
114
115 let storage;
118 if url_str.starts_with("localhost:") {
119 storage = format!("http://{url_str}");
120 url_str = storage.as_str();
121 }
122
123 let url = Url::parse(url_str)
124 .or_else(|err| match err {
125 ParseError::RelativeUrlWithoutBase => {
126 if SocketAddr::from_str(url_str).is_ok() {
127 Url::parse(&format!("http://{url_str}"))
128 } else {
129 let path = Path::new(url_str);
130
131 if let Ok(path) = resolve_path(path) {
132 Url::parse(&format!("file://{}", path.display()))
133 } else {
134 Err(err)
135 }
136 }
137 }
138 _ => Err(err),
139 })
140 .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
141
142 let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
144
145 Self {
146 url,
147 chain: NamedChain::Mainnet,
148 max_retry: 8,
149 initial_backoff: 800,
150 timeout: REQUEST_TIMEOUT,
151 compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
153 jwt: None,
154 headers: vec![],
155 is_local,
156 accept_invalid_certs: false,
157 no_proxy: false,
158 curl_mode: false,
159 }
160 }
161
162 pub fn timeout(mut self, timeout: Duration) -> Self {
169 self.timeout = timeout;
170 self
171 }
172
173 pub fn chain(mut self, chain: NamedChain) -> Self {
175 self.chain = chain;
176 self
177 }
178
179 pub fn max_retry(mut self, max_retry: u32) -> Self {
181 self.max_retry = max_retry;
182 self
183 }
184
185 pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
187 self.max_retry = max_retry.unwrap_or(self.max_retry);
188 self
189 }
190
191 pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
194 self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
195 self
196 }
197
198 pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
200 self.initial_backoff = initial_backoff;
201 self
202 }
203
204 pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
208 self.compute_units_per_second = compute_units_per_second;
209 self
210 }
211
212 pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
216 if let Some(cups) = compute_units_per_second {
217 self.compute_units_per_second = cups;
218 }
219 self
220 }
221
222 pub fn local(mut self, is_local: bool) -> Self {
226 self.is_local = is_local;
227 self
228 }
229
230 pub fn aggressive(self) -> Self {
234 self.max_retry(100).initial_backoff(100).local(true)
235 }
236
237 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
239 self.jwt = Some(jwt.into());
240 self
241 }
242
243 pub fn headers(mut self, headers: Vec<String>) -> Self {
245 self.headers = headers;
246
247 self
248 }
249
250 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
252 self.headers = headers.unwrap_or(self.headers);
253 self
254 }
255
256 pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
258 self.accept_invalid_certs = accept_invalid_certs;
259 self
260 }
261
262 pub fn no_proxy(mut self, no_proxy: bool) -> Self {
267 self.no_proxy = no_proxy;
268 self
269 }
270
271 pub fn curl_mode(mut self, curl_mode: bool) -> Self {
276 self.curl_mode = curl_mode;
277 self
278 }
279
280 pub fn build(self) -> Result<RetryProvider> {
282 let Self {
283 url,
284 chain,
285 max_retry,
286 initial_backoff,
287 timeout,
288 compute_units_per_second,
289 jwt,
290 headers,
291 is_local,
292 accept_invalid_certs,
293 no_proxy,
294 curl_mode,
295 } = self;
296 let url = url?;
297
298 let retry_layer =
299 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
300
301 if curl_mode {
303 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
304 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
305
306 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
307 .connect_provider(RootProvider::new(client));
308
309 return Ok(provider);
310 }
311
312 let transport = RuntimeTransportBuilder::new(url)
313 .with_timeout(timeout)
314 .with_headers(headers)
315 .with_jwt(jwt)
316 .accept_invalid_certs(accept_invalid_certs)
317 .no_proxy(no_proxy)
318 .build();
319 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
320
321 if !is_local {
322 client.set_poll_interval(
323 chain
324 .average_blocktime_hint()
325 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
328 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
329 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
330 );
331 }
332
333 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
334 .connect_provider(RootProvider::new(client));
335
336 Ok(provider)
337 }
338
339 pub fn build_with_wallet(self, wallet: EthereumWallet) -> Result<RetryProviderWithSigner> {
341 let Self {
342 url,
343 chain,
344 max_retry,
345 initial_backoff,
346 timeout,
347 compute_units_per_second,
348 jwt,
349 headers,
350 is_local,
351 accept_invalid_certs,
352 no_proxy,
353 curl_mode,
354 } = self;
355 let url = url?;
356
357 let retry_layer =
358 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
359
360 if curl_mode {
362 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
363 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
364
365 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
366 .with_recommended_fillers()
367 .wallet(wallet)
368 .connect_provider(RootProvider::new(client));
369
370 return Ok(provider);
371 }
372
373 let transport = RuntimeTransportBuilder::new(url)
374 .with_timeout(timeout)
375 .with_headers(headers)
376 .with_jwt(jwt)
377 .accept_invalid_certs(accept_invalid_certs)
378 .no_proxy(no_proxy)
379 .build();
380
381 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
382
383 if !is_local {
384 client.set_poll_interval(
385 chain
386 .average_blocktime_hint()
387 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
390 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
391 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
392 );
393 }
394
395 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
396 .with_recommended_fillers()
397 .wallet(wallet)
398 .connect_provider(RootProvider::new(client));
399
400 Ok(provider)
401 }
402}
403
404#[cfg(not(windows))]
405fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
406 if path.is_absolute() {
407 Ok(path.to_path_buf())
408 } else {
409 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
410 }
411}
412
413#[cfg(windows)]
414fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
415 if let Some(s) = path.to_str()
416 && s.starts_with(r"\\.\pipe\")
417 {
418 return Ok(path.to_path_buf());
419 }
420 Err(())
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn can_auto_correct_missing_prefix() {
429 let builder = ProviderBuilder::new("localhost:8545");
430 assert!(builder.url.is_ok());
431
432 let url = builder.url.unwrap();
433 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
434 }
435}