1pub mod runtime_transport;
4
5use crate::{
6 provider::runtime_transport::RuntimeTransportBuilder, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
7};
8use alloy_provider::{
9 fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
10 network::{AnyNetwork, EthereumWallet},
11 Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
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}
100
101impl ProviderBuilder {
102 pub fn new(url_str: &str) -> Self {
104 let mut url_str = url_str;
106
107 let storage;
110 if url_str.starts_with("localhost:") {
111 storage = format!("http://{url_str}");
112 url_str = storage.as_str();
113 }
114
115 let url = Url::parse(url_str)
116 .or_else(|err| match err {
117 ParseError::RelativeUrlWithoutBase => {
118 if SocketAddr::from_str(url_str).is_ok() {
119 Url::parse(&format!("http://{url_str}"))
120 } else {
121 let path = Path::new(url_str);
122
123 if let Ok(path) = resolve_path(path) {
124 Url::parse(&format!("file://{}", path.display()))
125 } else {
126 Err(err)
127 }
128 }
129 }
130 _ => Err(err),
131 })
132 .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
133
134 let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
136
137 Self {
138 url,
139 chain: NamedChain::Mainnet,
140 max_retry: 8,
141 initial_backoff: 800,
142 timeout: REQUEST_TIMEOUT,
143 compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
145 jwt: None,
146 headers: vec![],
147 is_local,
148 }
149 }
150
151 pub fn timeout(mut self, timeout: Duration) -> Self {
158 self.timeout = timeout;
159 self
160 }
161
162 pub fn chain(mut self, chain: NamedChain) -> Self {
164 self.chain = chain;
165 self
166 }
167
168 pub fn max_retry(mut self, max_retry: u32) -> Self {
170 self.max_retry = max_retry;
171 self
172 }
173
174 pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
176 self.max_retry = max_retry.unwrap_or(self.max_retry);
177 self
178 }
179
180 pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
183 self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
184 self
185 }
186
187 pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
189 self.initial_backoff = initial_backoff;
190 self
191 }
192
193 pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
197 self.compute_units_per_second = compute_units_per_second;
198 self
199 }
200
201 pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
205 if let Some(cups) = compute_units_per_second {
206 self.compute_units_per_second = cups;
207 }
208 self
209 }
210
211 pub fn local(mut self, is_local: bool) -> Self {
215 self.is_local = is_local;
216 self
217 }
218
219 pub fn aggressive(self) -> Self {
223 self.max_retry(100).initial_backoff(100).local(true)
224 }
225
226 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
228 self.jwt = Some(jwt.into());
229 self
230 }
231
232 pub fn headers(mut self, headers: Vec<String>) -> Self {
234 self.headers = headers;
235
236 self
237 }
238
239 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
241 self.headers = headers.unwrap_or(self.headers);
242 self
243 }
244
245 pub fn build(self) -> Result<RetryProvider> {
247 let Self {
248 url,
249 chain,
250 max_retry,
251 initial_backoff,
252 timeout,
253 compute_units_per_second,
254 jwt,
255 headers,
256 is_local,
257 } = self;
258 let url = url?;
259
260 let retry_layer =
261 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
262
263 let transport = RuntimeTransportBuilder::new(url)
264 .with_timeout(timeout)
265 .with_headers(headers)
266 .with_jwt(jwt)
267 .build();
268 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
269
270 if !is_local {
271 client.set_poll_interval(
272 chain
273 .average_blocktime_hint()
274 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
277 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
278 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
279 );
280 }
281
282 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
283 .on_provider(RootProvider::new(client));
284
285 Ok(provider)
286 }
287
288 pub fn build_with_wallet(self, wallet: EthereumWallet) -> Result<RetryProviderWithSigner> {
290 let Self {
291 url,
292 chain,
293 max_retry,
294 initial_backoff,
295 timeout,
296 compute_units_per_second,
297 jwt,
298 headers,
299 is_local,
300 } = self;
301 let url = url?;
302
303 let retry_layer =
304 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
305
306 let transport = RuntimeTransportBuilder::new(url)
307 .with_timeout(timeout)
308 .with_headers(headers)
309 .with_jwt(jwt)
310 .build();
311
312 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
313
314 if !is_local {
315 client.set_poll_interval(
316 chain
317 .average_blocktime_hint()
318 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
319 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
320 );
321 }
322
323 let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
324 .with_recommended_fillers()
325 .wallet(wallet)
326 .on_provider(RootProvider::new(client));
327
328 Ok(provider)
329 }
330}
331
332#[cfg(not(windows))]
333fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
334 if path.is_absolute() {
335 Ok(path.to_path_buf())
336 } else {
337 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
338 }
339}
340
341#[cfg(windows)]
342fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
343 if let Some(s) = path.to_str() {
344 if s.starts_with(r"\\.\pipe\") {
345 return Ok(path.to_path_buf());
346 }
347 }
348 Err(())
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn can_auto_correct_missing_prefix() {
357 let builder = ProviderBuilder::new("localhost:8545");
358 assert!(builder.url.is_ok());
359
360 let url = builder.url.unwrap();
361 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
362 }
363}