1use std::num::NonZeroUsize;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use alloy_network::{Ethereum as AlloyEthereum, EthereumWallet, NetworkWallet, TransactionBuilder};
6use alloy_primitives::{Address, Bytes};
7use alloy_provider::fillers::{
8 BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
9};
10use alloy_provider::{
11 Identity, PendingTransactionError, Provider, ProviderBuilder, RootProvider, WalletProvider,
12};
13use alloy_rpc_client::RpcClient;
14use alloy_rpc_types_eth::{BlockId, TransactionReceipt, TransactionRequest};
15use alloy_transport::TransportError;
16use alloy_transport::layers::{FallbackLayer, ThrottleLayer};
17use alloy_transport_http::Http;
18use r402_core::chain::{ChainId, ChainProvider};
19use tower::ServiceBuilder;
20#[cfg(feature = "telemetry")]
21use tracing::Instrument;
22use url::Url;
23
24use crate::chain::nonce::PendingNonceManager;
25use crate::chain::types::Eip155ChainReference;
26
27pub type InnerFiller = JoinFill<
29 GasFiller,
30 JoinFill<BlobGasFiller, JoinFill<NonceFiller<PendingNonceManager>, ChainIdFiller>>,
31>;
32
33pub type InnerProvider = FillProvider<
38 JoinFill<JoinFill<Identity, InnerFiller>, WalletFiller<EthereumWallet>>,
39 RootProvider,
40>;
41
42#[derive(Debug)]
62pub struct Eip155ChainProvider {
63 chain: Eip155ChainReference,
64 eip1559: bool,
65 flashblocks: bool,
66 receipt_timeout_secs: u64,
67 inner: InnerProvider,
68 signer_addresses: Arc<[Address]>,
70 signer_cursor: Arc<AtomicUsize>,
72 nonce_manager: PendingNonceManager,
74}
75
76impl Eip155ChainProvider {
77 pub fn new(
93 chain: Eip155ChainReference,
94 wallet: EthereumWallet,
95 rpc_endpoints: &[(Url, Option<u32>)],
96 eip1559: bool,
97 flashblocks: bool,
98 receipt_timeout_secs: u64,
99 ) -> Result<Self, Box<dyn std::error::Error>> {
100 let signer_addresses =
101 NetworkWallet::<AlloyEthereum>::signer_addresses(&wallet).collect::<Vec<_>>();
102 if signer_addresses.is_empty() {
103 return Err("at least one signer must be provided".into());
104 }
105 let signer_addresses: Arc<[Address]> = signer_addresses.into();
106 let signer_cursor = Arc::new(AtomicUsize::new(0));
107
108 let chain_id: ChainId = chain.into();
109 let client = Self::rpc_client(&chain_id, rpc_endpoints)?;
110
111 let nonce_manager = PendingNonceManager::default();
112 let filler = JoinFill::new(
113 GasFiller::default(),
114 JoinFill::new(
115 BlobGasFiller::default(),
116 JoinFill::new(
117 NonceFiller::new(nonce_manager.clone()),
118 ChainIdFiller::default(),
119 ),
120 ),
121 );
122 let inner: InnerProvider = ProviderBuilder::default()
123 .filler(filler)
124 .wallet(wallet)
125 .connect_client(client);
126
127 #[cfg(feature = "telemetry")]
128 tracing::info!(chain=%chain_id, signers=?signer_addresses, "Using EVM provider");
129
130 Ok(Self {
131 chain,
132 eip1559,
133 flashblocks,
134 receipt_timeout_secs,
135 inner,
136 signer_addresses,
137 signer_cursor,
138 nonce_manager,
139 })
140 }
141
142 pub fn rpc_client(
151 chain_id: &ChainId,
152 endpoints: &[(Url, Option<u32>)],
153 ) -> Result<RpcClient, Box<dyn std::error::Error>> {
154 #[cfg(not(feature = "telemetry"))]
155 let _ = chain_id;
156 let transports = endpoints
157 .iter()
158 .filter_map(|(url, rate_limit)| {
159 let scheme = url.scheme();
160 let is_http = scheme == "http" || scheme == "https";
161 if !is_http {
162 return None;
163 }
164 #[cfg(feature = "telemetry")]
165 tracing::info!(chain=%chain_id, rpc_url=%url, rate_limit=?rate_limit, "Using HTTP transport");
166 let limit = rate_limit.unwrap_or(u32::MAX);
167 let service = ServiceBuilder::new()
168 .layer(ThrottleLayer::new(limit))
169 .service(Http::new(url.clone()));
170 Some(service)
171 })
172 .collect::<Vec<_>>();
173 let count = NonZeroUsize::new(transports.len())
174 .ok_or("at least one HTTP RPC endpoint is required")?;
175 let fallback = ServiceBuilder::new()
176 .layer(FallbackLayer::default().with_active_transport_count(count))
177 .service(transports);
178 Ok(RpcClient::new(fallback, false))
179 }
180
181 #[allow(
183 clippy::indexing_slicing,
184 reason = "bounds guaranteed by constructor requiring non-empty signers"
185 )]
186 fn next_signer_address(&self) -> Address {
187 debug_assert!(
188 !self.signer_addresses.is_empty(),
189 "signer_addresses must not be empty"
190 );
191 if self.signer_addresses.len() == 1 {
192 self.signer_addresses[0]
193 } else {
194 let next =
195 self.signer_cursor.fetch_add(1, Ordering::Relaxed) % self.signer_addresses.len();
196 self.signer_addresses[next]
197 }
198 }
199}
200
201#[derive(Debug, thiserror::Error)]
203pub enum MetaTransactionSendError {
204 #[error(transparent)]
206 Transport(#[from] TransportError),
207 #[error(transparent)]
209 PendingTransaction(#[from] PendingTransactionError),
210 #[error("{0}")]
212 Custom(String),
213}
214
215#[derive(Debug, Clone)]
217pub struct MetaTransaction {
218 pub to: Address,
220 pub calldata: Bytes,
222 pub confirmations: u64,
224 pub from: Option<Address>,
230}
231
232impl MetaTransaction {
233 #[must_use]
235 pub const fn new(to: Address, calldata: Bytes, confirmations: u64) -> Self {
236 Self {
237 to,
238 calldata,
239 confirmations,
240 from: None,
241 }
242 }
243
244 #[must_use]
246 pub const fn with_from(mut self, from: Address) -> Self {
247 self.from = Some(from);
248 self
249 }
250}
251
252impl ChainProvider for Eip155ChainProvider {
253 fn signer_addresses(&self) -> Vec<String> {
254 self.inner
255 .signer_addresses()
256 .map(|a| a.to_string())
257 .collect()
258 }
259
260 fn chain_id(&self) -> ChainId {
261 self.chain.into()
262 }
263}
264
265pub trait Eip155MetaTransactionProvider {
267 type Error;
269 type Inner: Provider;
271
272 fn inner(&self) -> &Self::Inner;
274 fn chain(&self) -> &Eip155ChainReference;
276
277 fn send_transaction(
279 &self,
280 tx: MetaTransaction,
281 ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send;
282}
283
284impl<T: Eip155MetaTransactionProvider> Eip155MetaTransactionProvider for Arc<T> {
285 type Error = T::Error;
286 type Inner = T::Inner;
287
288 fn inner(&self) -> &Self::Inner {
289 (**self).inner()
290 }
291
292 fn chain(&self) -> &Eip155ChainReference {
293 (**self).chain()
294 }
295
296 fn send_transaction(
297 &self,
298 tx: MetaTransaction,
299 ) -> impl Future<Output = Result<TransactionReceipt, Self::Error>> + Send {
300 (**self).send_transaction(tx)
301 }
302}
303
304impl Eip155MetaTransactionProvider for Eip155ChainProvider {
305 type Error = MetaTransactionSendError;
306 type Inner = InnerProvider;
307
308 fn inner(&self) -> &Self::Inner {
309 &self.inner
310 }
311
312 fn chain(&self) -> &Eip155ChainReference {
313 &self.chain
314 }
315
316 async fn send_transaction(
356 &self,
357 tx: MetaTransaction,
358 ) -> Result<TransactionReceipt, Self::Error> {
359 let from_address = match tx.from {
360 Some(pinned) => {
361 if !self.signer_addresses.contains(&pinned) {
362 return Err(MetaTransactionSendError::Custom(format!(
363 "requested signer {pinned} is not in the configured wallet"
364 )));
365 }
366 pinned
367 }
368 None => self.next_signer_address(),
369 };
370 let mut txr = TransactionRequest::default()
371 .with_to(tx.to)
372 .with_from(from_address)
373 .with_input(tx.calldata);
374
375 if !self.eip1559 {
376 let provider = &self.inner;
377 let gas_fut = provider.get_gas_price();
378 #[cfg(feature = "telemetry")]
379 let gas: u128 = gas_fut
380 .instrument(tracing::info_span!("get_gas_price"))
381 .await?;
382 #[cfg(not(feature = "telemetry"))]
383 let gas: u128 = gas_fut.await?;
384 txr.set_gas_price(gas);
385 }
386
387 if txr.gas.is_none() {
389 let block_id = if self.flashblocks {
390 BlockId::latest()
391 } else {
392 BlockId::pending()
393 };
394 let gas_limit = self.inner.estimate_gas(txr.clone()).block(block_id).await?;
395 txr.set_gas_limit(gas_limit);
396 }
397
398 let pending_tx = match self.inner.send_transaction(txr).await {
400 Ok(pending) => pending,
401 Err(e) => {
402 self.nonce_manager.reset_nonce(from_address).await;
404 return Err(MetaTransactionSendError::Transport(e));
405 }
406 };
407
408 let timeout = std::time::Duration::from_secs(self.receipt_timeout_secs);
411
412 let watcher = pending_tx
413 .with_required_confirmations(tx.confirmations)
414 .with_timeout(Some(timeout));
415
416 match watcher.get_receipt().await {
417 Ok(receipt) => Ok(receipt),
418 Err(e) => {
419 self.nonce_manager.reset_nonce(from_address).await;
421 Err(MetaTransactionSendError::PendingTransaction(e))
422 }
423 }
424 }
425}
426
427#[cfg(test)]
428#[allow(
429 clippy::expect_used,
430 clippy::unwrap_used,
431 reason = "test assertions on known-valid fixtures"
432)]
433mod tests {
434 use super::*;
435
436 fn chain_id() -> ChainId {
437 "eip155:8453".parse().expect("fixture chain id")
438 }
439
440 #[test]
441 fn rpc_client_rejects_empty_endpoints() {
442 let err = Eip155ChainProvider::rpc_client(&chain_id(), &[]);
443 assert!(
444 err.is_err(),
445 "empty endpoint list must return Err, not panic"
446 );
447 }
448
449 #[test]
450 fn rpc_client_rejects_non_http_endpoints() {
451 let ws = Url::parse("ws://127.0.0.1:8545").expect("fixture ws url");
452 let err = Eip155ChainProvider::rpc_client(&chain_id(), &[(ws, None)]);
453 assert!(
454 err.is_err(),
455 "non-HTTP endpoints must return Err after filtering"
456 );
457 }
458
459 #[test]
460 fn rpc_client_accepts_https_endpoint() {
461 let url = Url::parse("https://mainnet.base.org").expect("fixture rpc url");
462 Eip155ChainProvider::rpc_client(&chain_id(), &[(url, None)])
463 .expect("HTTPS endpoint must construct");
464 }
465}