Skip to main content

blueprint_client_evm/
instrumented_client.rs

1//! Re-exported from <https://github.com/Layr-Labs/eigensdk-rs/blob/main/crates/chainio/clients/eth/src/instrumented_client.rs>
2
3use crate::client::BackendClient;
4use alloy_consensus::TxEnvelope;
5use alloy_json_rpc::{RpcRecv, RpcSend};
6use alloy_primitives::{Address, B256, BlockHash, BlockNumber, Bytes, ChainId, U64, U256};
7use alloy_provider::{Provider, ProviderBuilder, RootProvider, WsConnect};
8use alloy_pubsub::Subscription;
9use alloy_rlp::Encodable;
10use alloy_rpc_types_eth::{
11    Block, BlockNumberOrTag, FeeHistory, Filter, Header, Log, SyncStatus, Transaction,
12    TransactionReceipt, TransactionRequest,
13};
14use alloy_transport::{TransportError, TransportResult};
15use blueprint_metrics_rpc_calls::RpcCallsMetrics as RpcCallsCollector;
16use blueprint_std::string::String;
17use blueprint_std::string::ToString;
18use blueprint_std::time::Instant;
19use blueprint_std::vec::Vec;
20use hex;
21use thiserror::Error;
22use url::Url;
23
24const PENDING_TAG: &str = "pending";
25
26enum ProviderInner {
27    Http(RootProvider),
28    Ws(RootProvider),
29}
30
31/// This struct represents an instrumented client that can be used to interact with an Ethereum node.
32/// It provides a set of methods to interact with the node and measures the duration of the calls.
33pub struct InstrumentedClient {
34    inner: ProviderInner,
35    rpc_collector: RpcCallsCollector,
36    net_version: u64,
37}
38
39/// Possible errors raised in signer creation
40#[derive(Error, Debug)]
41pub enum InstrumentedClientError {
42    #[error("invalid url")]
43    InvalidUrl,
44    #[error("error getting version")]
45    ErrorGettingVersion,
46    #[error("error running command")]
47    CommandError,
48
49    #[error(transparent)]
50    Rpc(#[from] alloy_json_rpc::RpcError<alloy_transport::TransportErrorKind>),
51}
52
53impl Provider for InstrumentedClient {
54    fn root(&self) -> &RootProvider {
55        match &self.inner {
56            ProviderInner::Http(provider) | ProviderInner::Ws(provider) => provider,
57        }
58    }
59}
60
61impl BackendClient for InstrumentedClient {
62    type Error = InstrumentedClientError;
63
64    /// Returns the latest block number.
65    ///
66    /// # Returns
67    ///
68    /// The latest block number.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the RPC call fails.
73    async fn block_number(&self) -> Result<BlockNumber, Self::Error> {
74        self.instrument_function("eth_blockNumber", ())
75            .await
76            .inspect_err(|err| {
77                blueprint_core::error!("Failed to get block number {:?}", err.to_string().as_str());
78            })
79            .map_err(|_err| InstrumentedClientError::CommandError)
80            .map(|result: U64| result.to())
81    }
82
83    /// Returns the block having the given block number.
84    ///
85    /// # Arguments
86    ///
87    /// * `number` - The block number.
88    ///
89    /// # Returns
90    ///
91    /// The block having the given block number.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the RPC call fails.
96    async fn block_by_number(
97        &self,
98        number: BlockNumberOrTag,
99    ) -> Result<Option<Block>, Self::Error> {
100        self.instrument_function("eth_getBlockByNumber", (number, true))
101            .await
102            .inspect_err(|err| {
103                blueprint_core::error!(
104                    "Failed to get block by number {:?}",
105                    err.to_string().as_str()
106                );
107            })
108            .map_err(|_err| InstrumentedClientError::CommandError)
109    }
110}
111
112#[allow(clippy::missing_errors_doc)] // The error types are self-explanatory
113impl InstrumentedClient {
114    /// Creates a new instance of the `InstrumentedClient`.
115    ///
116    /// # Arguments
117    ///
118    /// * `url` - The URL of the RPC server.
119    ///
120    /// # Returns
121    ///
122    /// A new instance of the `InstrumentedClient`.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the URL is invalid or if there is an error getting the version.
127    pub async fn new<T: TryInto<Url>>(url: T) -> Result<Self, InstrumentedClientError> {
128        let url = url
129            .try_into()
130            .map_err(|_| InstrumentedClientError::InvalidUrl)?;
131        let http_client = ProviderBuilder::new()
132            .disable_recommended_fillers()
133            .connect_http(url);
134        let net_version = http_client
135            .get_net_version()
136            .await
137            .map_err(|_| InstrumentedClientError::ErrorGettingVersion)?;
138
139        let rpc_collector = RpcCallsCollector::new();
140        Ok(InstrumentedClient {
141            inner: ProviderInner::Http(http_client),
142            rpc_collector,
143            net_version,
144        })
145    }
146
147    /// Creates a new instance of the `InstrumentedClient` that supports ws connection.
148    ///
149    /// # Arguments
150    ///
151    /// * `url` - The ws URL of the RPC server .
152    ///
153    /// # Returns
154    ///
155    /// A new instance of the `InstrumentedClient`.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the URL is invalid or if there is an error getting the version.
160    pub async fn new_ws<T: TryInto<Url>>(url: T) -> Result<Self, InstrumentedClientError> {
161        let url = url
162            .try_into()
163            .map_err(|_| InstrumentedClientError::InvalidUrl)?;
164        let ws_connect = WsConnect::new(url);
165
166        let ws_client = ProviderBuilder::new()
167            .disable_recommended_fillers()
168            .connect_ws(ws_connect)
169            .await?;
170        let net_version = ws_client
171            .get_net_version()
172            .await
173            .map_err(|_| InstrumentedClientError::ErrorGettingVersion)?;
174
175        let rpc_collector = RpcCallsCollector::new();
176        Ok(InstrumentedClient {
177            inner: ProviderInner::Ws(ws_client),
178            rpc_collector,
179            net_version,
180        })
181    }
182
183    /// Creates a new instance of the `InstrumentedClient` from an existing client (`RootProvider`).
184    ///
185    /// # Arguments
186    ///
187    /// * `client` - The existing client (`RootProvider`).
188    ///
189    /// # Returns
190    ///
191    /// A new instance of the `InstrumentedClient`.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if there is an error getting the version.
196    pub async fn new_from_client(client: RootProvider) -> Result<Self, InstrumentedClientError> {
197        let net_version = client
198            .get_net_version()
199            .await
200            .map_err(|_| InstrumentedClientError::ErrorGettingVersion)?;
201
202        let rpc_collector = RpcCallsCollector::new();
203        Ok(InstrumentedClient {
204            inner: ProviderInner::Http(client),
205            rpc_collector,
206            net_version,
207        })
208    }
209
210    /// Returns the chain ID.
211    ///
212    /// # Returns
213    ///
214    /// The chain ID.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the RPC call fails.
219    pub async fn chain_id(&self) -> TransportResult<ChainId> {
220        self.instrument_function("eth_chainId", ())
221            .await
222            .inspect_err(|err| {
223                blueprint_core::error!("Failed to get chain id {:?}", err.to_string().as_str());
224            })
225            .map(|result: U64| result.to())
226    }
227
228    /// Returns the balance of the account at the given block number.
229    ///
230    /// # Arguments
231    ///
232    /// * `account` - The account address.
233    /// * `block_number` - The block number.
234    ///
235    /// # Returns
236    ///
237    /// The balance of the account at the given block number.
238    pub async fn balance_at(
239        &self,
240        account: Address,
241        block_number: BlockNumberOrTag,
242    ) -> TransportResult<U256> {
243        self.instrument_function("eth_getBalance", (account, block_number))
244            .await
245            .inspect_err(|err| {
246                blueprint_core::error!("Failed to get balance {:?}", err.to_string().as_str());
247            })
248    }
249
250    /// Returns the block having the given block hash.
251    ///
252    /// # Arguments
253    ///
254    /// * `hash` - The block hash.
255    ///
256    /// # Returns
257    ///
258    /// The block having the given block hash.
259    pub async fn block_by_hash(&self, hash: BlockHash) -> TransportResult<Option<Block>> {
260        self.instrument_function("eth_getBlockByHash", (hash, true))
261            .await
262            .inspect_err(|err| {
263                blueprint_core::error!(
264                    "Failed to get block by hash {:?}",
265                    err.to_string().as_str()
266                );
267            })
268    }
269
270    /// Executes a message call transaction.
271    ///
272    /// # Arguments
273    ///
274    /// * `call` - The message call to be executed
275    /// * `block_number` - The block height at which the call runs. *Note:* in case this argument is n
276    ///
277    /// # Returns
278    ///
279    /// The returned value of the executed contract.
280    pub async fn call_contract(
281        &self,
282        call: TransactionRequest,
283        block_number: BlockNumberOrTag,
284    ) -> TransportResult<Bytes> {
285        self.instrument_function("eth_call", (call, block_number))
286            .await
287            .inspect_err(|err| {
288                blueprint_core::error!("Failed to call contract {:?}", err.to_string().as_str());
289            })
290    }
291
292    /// Returns the compiled bytecode of a smart contract given its address and block number.
293    ///
294    /// # Arguments
295    ///
296    /// * `address` - The address of the smart contract.
297    /// * `block_number` - The block number.
298    ///
299    /// # Returns
300    ///
301    /// The compiled bytecode of the smart contract with the given address and block number.
302    pub async fn code_at(
303        &self,
304        address: Address,
305        block_number: BlockNumberOrTag,
306    ) -> TransportResult<Bytes> {
307        self.instrument_function("eth_getCode", (address, block_number))
308            .await
309            .inspect_err(|err| {
310                blueprint_core::error!("Failed to get code {:?}", err.to_string().as_str());
311            })
312    }
313
314    /// Estimates the gas needed to execute a specific transaction.
315    ///
316    /// # Arguments
317    ///
318    /// * `tx` - The transaction from which the gas consumption is estimated.
319    ///
320    /// # Returns
321    ///
322    /// The estimated gas.
323    pub async fn estimate_gas(&self, tx: TransactionRequest) -> TransportResult<u64> {
324        self.instrument_function("eth_estimateGas", (tx,))
325            .await
326            .inspect_err(|err| {
327                blueprint_core::error!("Failed to estimate gas {:?}", err.to_string().as_str());
328            })
329            .map(|result: U64| result.to())
330    }
331
332    /// Returns a collection of historical gas information.
333    ///
334    /// # Arguments
335    ///
336    /// * `block_count` - The number of blocks to include in the collection.
337    /// * `last_block` - The last block number to include in the collection.
338    /// * `reward_percentiles` - A sorted list of percentage points used to
339    ///   sample the effective priority fees per gas from each block. The samples are
340    ///   taken in ascending order and weighted by gas usage. The list is sorted increasingly.
341    pub async fn fee_history(
342        &self,
343        block_count: u64,
344        last_block: BlockNumberOrTag,
345        reward_percentiles: &[f64],
346    ) -> TransportResult<FeeHistory> {
347        self.instrument_function(
348            "eth_feeHistory",
349            (block_count, last_block, reward_percentiles),
350        )
351        .await
352        .inspect_err(|err| {
353            blueprint_core::error!("Failed to get fee history {:?}", err.to_string().as_str());
354        })
355    }
356
357    /// Executes a filter query.
358    ///
359    /// # Arguments
360    ///
361    /// * `filter` - The filter query to be executed.
362    ///
363    /// # Returns
364    ///
365    /// A vector of logs.
366    pub async fn filter_logs(&self, filter: Filter) -> TransportResult<Vec<Log>> {
367        self.instrument_function("eth_getLogs", (filter,))
368            .await
369            .inspect_err(|err| {
370                blueprint_core::error!("Failed to get filter logs {:?}", err.to_string().as_str());
371            })
372    }
373
374    /// Returns the block header with the given hash.
375    ///
376    /// # Arguments
377    ///
378    /// * `hash` - The block hash.
379    ///
380    /// # Returns
381    ///
382    /// The block header.
383    pub async fn header_by_hash(&self, hash: B256) -> TransportResult<Header> {
384        let transaction_detail = false;
385        self.instrument_function("eth_getBlockByHash", (hash, transaction_detail))
386            .await
387            .inspect_err(|err| {
388                blueprint_core::error!(
389                    "Failed to get header by hash {:?}",
390                    err.to_string().as_str()
391                );
392            })
393    }
394
395    /// Returns a block header with the given block number.
396    ///
397    /// # Arguments
398    ///
399    /// * `block_number` - The block number.
400    ///
401    /// # Returns
402    ///
403    /// The block header.
404    pub async fn header_by_number(
405        &self,
406        block_number: BlockNumberOrTag,
407    ) -> TransportResult<Header> {
408        let transaction_detail = false;
409        self.instrument_function("eth_getBlockByNumber", (block_number, transaction_detail))
410            .await
411            .inspect_err(|err| {
412                blueprint_core::error!(
413                    "Failed to get header by number {:?}",
414                    err.to_string().as_str()
415                );
416            })
417    }
418
419    /// Returns the nonce of the given account.
420    ///
421    /// # Arguments
422    ///
423    /// * `account` - The address of the account.
424    /// * `block_number` - The block number from where the nonce is taken.
425    ///
426    /// # Returns
427    ///
428    /// The nonce of the account.
429    pub async fn nonce_at(
430        &self,
431        account: Address,
432        block_number: BlockNumberOrTag,
433    ) -> TransportResult<u64> {
434        self.instrument_function("eth_getTransactionCount", (account, block_number))
435            .await
436            .inspect_err(|err| {
437                blueprint_core::error!("Failed to get nonce {:?}", err.to_string().as_str());
438            })
439            .map(|result: U64| result.to())
440    }
441
442    /// Returns the wei balance of the given account in the pending state.
443    ///
444    /// # Arguments
445    ///
446    /// * `account` - The address of the account.
447    ///
448    /// # Returns
449    ///
450    /// The wei balance of the account.
451    pub async fn pending_balance_at(&self, account: Address) -> TransportResult<U256> {
452        self.instrument_function("eth_getBalance", (account, PENDING_TAG))
453            .await
454            .inspect_err(|err| {
455                blueprint_core::error!(
456                    "Failed to get pending balance {:?}",
457                    err.to_string().as_str()
458                );
459            })
460    }
461
462    /// Executes a message call transaction using the EVM.
463    ///
464    /// The state seen by the contract call is the pending state.
465    ///
466    /// # Arguments
467    ///
468    /// * `call` - The message call to be executed
469    ///
470    /// # Returns
471    ///
472    /// The returned value of the executed contract.
473    pub async fn pending_call_contract(&self, call: TransactionRequest) -> TransportResult<Bytes> {
474        self.call_contract(call, BlockNumberOrTag::Pending).await
475    }
476
477    /// Returns the contract code of the given account in the pending state.
478    ///
479    /// # Arguments
480    ///
481    /// * `account` - The address of the contract.
482    ///
483    /// # Returns
484    ///
485    /// The contract code.
486    pub async fn pending_code_at(&self, account: Address) -> TransportResult<Bytes> {
487        self.instrument_function("eth_getCode", (account, PENDING_TAG))
488            .await
489            .inspect_err(|err| {
490                blueprint_core::error!("Failed to get pending code {:?}", err.to_string().as_str());
491            })
492    }
493
494    /// Returns the account nonce of the given account in the pending state.
495    ///
496    /// This is the nonce that should be used for the next transaction.
497    ///
498    /// # Arguments
499    ///
500    /// * `account` - The address of the account.
501    ///
502    /// # Returns
503    ///
504    /// * The nonce of the account in the pending state.
505    pub async fn pending_nonce_at(&self, account: Address) -> TransportResult<u64> {
506        self.instrument_function("eth_getTransactionCount", (account, PENDING_TAG))
507            .await
508            .inspect_err(|err| {
509                blueprint_core::error!(
510                    "Failed to get pending nonce {:?}",
511                    err.to_string().as_str()
512                );
513            })
514            .map(|result: U64| result.to())
515    }
516
517    /// Returns the value of key in the contract storage of the given account in the pending state.
518    ///
519    /// # Arguments
520    ///
521    /// * `account` - The address of the contract.
522    /// * `key` - The position in the storage.
523    ///
524    /// # Returns
525    ///
526    /// The value of the storage position at the provided address.
527    pub async fn pending_storage_at(&self, account: Address, key: U256) -> TransportResult<U256> {
528        self.instrument_function("eth_getStorageAt", (account, key, PENDING_TAG))
529            .await
530            .inspect_err(|err| {
531                blueprint_core::error!(
532                    "Failed to get pending storage {:?}",
533                    err.to_string().as_str()
534                );
535            })
536    }
537
538    /// Returns the total number of transactions in the pending state.
539    ///
540    /// # Returns
541    ///
542    /// The number of pending transactions.
543    pub async fn pending_transaction_count(&self) -> TransportResult<u64> {
544        self.instrument_function("eth_getBlockTransactionCountByNumber", (PENDING_TAG,))
545            .await
546            .inspect_err(|err| {
547                blueprint_core::error!(
548                    "Failed to get transaction count {:?}",
549                    err.to_string().as_str()
550                );
551            })
552            .map(|result: U64| result.to())
553    }
554
555    /// Sends a signed transaction into the pending pool for execution.
556    ///
557    /// # Arguments
558    ///
559    /// * `tx` - The transaction to be executed.
560    ///
561    /// # Returns
562    ///
563    /// The hash of the given transaction.
564    pub async fn send_transaction(&self, tx: TxEnvelope) -> TransportResult<B256> {
565        let mut encoded_tx = Vec::new();
566        tx.encode(&mut encoded_tx);
567        self.instrument_function("eth_sendRawTransaction", (hex::encode(encoded_tx),))
568            .await
569            .inspect_err(|err| {
570                blueprint_core::error!("Failed to send transaction {:?}", err.to_string().as_str());
571            })
572    }
573
574    /// Returns the value of key in the contract storage of the given account.
575    ///
576    /// # Arguments
577    ///
578    /// * `account` - The address of the contract.
579    /// * `key` - The position in the storage.
580    /// * `block_number` - The block number from which the storage is taken.
581    ///
582    /// # Returns
583    ///
584    /// The value of the storage position at the provided address.
585    pub async fn storage_at(
586        &self,
587        account: Address,
588        key: U256,
589        block_number: U256,
590    ) -> TransportResult<U256> {
591        self.instrument_function("eth_getStorageAt", (account, key, block_number))
592            .await
593            .inspect_err(|err| {
594                blueprint_core::error!("Failed to get storage {:?}", err.to_string().as_str());
595            })
596    }
597
598    /// Subscribes to the results of a streaming filter query.
599    ///
600    /// # Arguments
601    ///
602    /// * `filter` - A filter query.
603    ///
604    /// # Returns
605    ///
606    /// The subscription.
607    ///
608    /// # Errors
609    ///
610    /// * If `ws_client` is `None`.
611    pub async fn subscribe_filter_logs<R: RpcRecv>(
612        &self,
613        filter: Filter,
614    ) -> TransportResult<Subscription<R>> {
615        let id: U256 = self
616            .instrument_function("eth_subscribe", ("logs", filter))
617            .await
618            .inspect_err(|err| {
619                blueprint_core::error!(
620                    "Failed to get logs subscription id {:?}",
621                    err.to_string().as_str(),
622                );
623            })?;
624
625        let ProviderInner::Ws(ws_client) = &self.inner else {
626            return Err(TransportError::UnsupportedFeature(
627                "http client does not support eth_subscribe calls.",
628            ));
629        };
630
631        ws_client.get_subscription(id.into()).await
632    }
633
634    /// Subscribes to notifications about the current blockchain head.
635    ///
636    /// # Returns
637    ///
638    /// The subscription.
639    ///
640    /// # Errors
641    ///
642    /// * If `ws_client` is `None`.
643    pub async fn subscribe_new_head<R: RpcRecv>(&self) -> TransportResult<Subscription<R>> {
644        let id: U256 = self
645            .instrument_function("eth_subscribe", ("newHeads",))
646            .await
647            .inspect_err(|err| {
648                blueprint_core::error!(
649                    "Failed to subscribe new head {:?}",
650                    err.to_string().as_str()
651                );
652            })?;
653
654        let ProviderInner::Ws(ws_client) = &self.inner else {
655            return Err(TransportError::UnsupportedFeature(
656                "http client does not support eth_subscribe calls.",
657            ));
658        };
659
660        ws_client.get_subscription(id.into()).await
661    }
662
663    /// Retrieves the currently suggested gas price.
664    ///
665    /// # Returns
666    ///
667    /// The currently suggested gas price.
668    pub async fn suggest_gas_price(&self) -> TransportResult<u64> {
669        self.instrument_function("eth_gasPrice", ())
670            .await
671            .inspect_err(|err| {
672                blueprint_core::error!(
673                    "Failed to suggest gas price {:?}",
674                    err.to_string().as_str()
675                );
676            })
677            .map(|result: U64| result.to())
678    }
679
680    /// Retrieves the currently suggested gas tip cap after EIP1559.
681    ///
682    /// # Returns
683    ///
684    /// The currently suggested gas price.
685    pub async fn suggest_gas_tip_cap(&self) -> TransportResult<u64> {
686        self.instrument_function("eth_maxPriorityFeePerGas", ())
687            .await
688            .inspect_err(|err| {
689                blueprint_core::error!(
690                    "Failed to suggest gas tip cap {:?}",
691                    err.to_string().as_str()
692                );
693            })
694            .map(|result: U64| result.to())
695    }
696
697    /// Retrieves the current progress of the sync algorithm.
698    ///
699    /// If there's no sync currently running, it returns None.
700    ///
701    /// # Returns
702    ///
703    /// The current progress of the sync algorithm.
704    pub async fn sync_progress(&self) -> TransportResult<SyncStatus> {
705        self.instrument_function("eth_syncing", ())
706            .await
707            .inspect_err(|err| {
708                blueprint_core::error!(
709                    "Failed to get sync progress {:?}",
710                    err.to_string().as_str()
711                );
712            })
713    }
714
715    /// Returns the transaction with the given hash.
716    ///
717    /// # Arguments
718    ///
719    /// * `tx_hash` - The transaction hash.
720    ///
721    /// # Returns
722    ///
723    /// The transaction with the given hash.
724    pub async fn transaction_by_hash(&self, tx_hash: B256) -> TransportResult<Transaction> {
725        self.instrument_function("eth_getTransactionByHash", (tx_hash,))
726            .await
727            .inspect_err(|err| {
728                blueprint_core::error!(
729                    "Failed to get transaction by hash {:?}",
730                    err.to_string().as_str(),
731                );
732            })
733    }
734
735    /// Returns the total number of transactions in the given block.
736    ///
737    /// # Arguments
738    ///
739    /// * `block_hash` - The block hash.
740    ///
741    /// # Returns
742    ///
743    /// The number of transactions in the given block.
744    pub async fn transaction_count(&self, block_hash: B256) -> TransportResult<u64> {
745        self.instrument_function("eth_getBlockTransactionCountByHash", (block_hash,))
746            .await
747            .inspect_err(|err| {
748                blueprint_core::error!(
749                    "Failed to get transaction count {:?}",
750                    err.to_string().as_str()
751                );
752            })
753            .map(|result: U64| result.to())
754    }
755
756    /// Returns a single transaction at index in the given block.
757    ///
758    /// # Arguments
759    ///
760    /// * `block_hash` - The block hash.
761    /// * `index` - The index of the transaction in the block.
762    ///
763    /// # Returns
764    ///
765    /// The transaction at index in the given block.
766    pub async fn transaction_in_block(
767        &self,
768        block_hash: B256,
769        index: u64,
770    ) -> TransportResult<Transaction> {
771        self.instrument_function("eth_getTransactionByBlockHashAndIndex", (block_hash, index))
772            .await
773            .inspect_err(|err| {
774                blueprint_core::error!("Failed to get transaction {:?}", err.to_string().as_str());
775            })
776    }
777
778    /// Returns the receipt of a transaction by transaction hash.
779    ///
780    /// NOTE: the receipt is not available for pending transactions.
781    ///
782    /// # Arguments
783    ///
784    /// * `tx_hash` - The hash of the transaction.
785    ///
786    /// # Returns
787    ///
788    /// The transaction receipt.
789    pub async fn transaction_receipt(&self, tx_hash: B256) -> TransportResult<TransactionReceipt> {
790        self.instrument_function("eth_getTransactionReceipt", (tx_hash,))
791            .await
792            .inspect_err(|err| {
793                blueprint_core::error!("Failed to get receipt {:?}", err.to_string().as_str());
794            })
795    }
796
797    /// Instrument a function call with the given method name and parameters.
798    ///
799    /// This function will measure the duration of the call and report it to the metrics collector.
800    ///
801    /// # Arguments
802    ///
803    /// * `rpc_method_name` - The name of the RPC method being called.
804    /// * `params` - The parameters to pass to the RPC method.
805    ///
806    /// # Returns
807    ///
808    /// The result of the RPC call.
809    async fn instrument_function<P, R>(
810        &self,
811        rpc_method_name: &str,
812        params: P,
813    ) -> TransportResult<R>
814    where
815        P: RpcSend,
816        R: RpcRecv,
817    {
818        let start = Instant::now();
819        let method_string = String::from(rpc_method_name);
820
821        // send the request with the provided client (http or ws)
822        let result = self.raw_request(method_string.into(), params).await;
823        let rpc_request_duration = start.elapsed();
824
825        // we only observe the duration of successful calls (even though this is not well defined in the spec)
826        self.rpc_collector.set_rpc_request_duration_seconds(
827            rpc_method_name,
828            self.net_version.to_string().as_str(),
829            rpc_request_duration.as_secs_f64(),
830        );
831        result
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838    use alloy_consensus::{SignableTransaction, TxLegacy};
839    use alloy_network::TxSignerSync;
840    use alloy_primitives::address;
841    use alloy_primitives::{TxKind::Call, U256, bytes};
842    use alloy_rpc_types::eth::{BlockId, BlockNumberOrTag, pubsub::SubscriptionResult};
843    use alloy_signer_local::PrivateKeySigner;
844    use blueprint_chain_setup_anvil::start_default_anvil_testnet;
845    use blueprint_evm_extra::util::get_provider_http;
846    use blueprint_evm_extra::util::wait_transaction;
847    use tokio;
848
849    #[tokio::test]
850    async fn test_suggest_gas_tip_cap() {
851        let testnet = start_default_anvil_testnet(false).await;
852
853        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
854            .await
855            .unwrap();
856        let fee_per_gas = instrumented_client.suggest_gas_tip_cap().await.unwrap();
857        let expected_fee_per_gas = get_provider_http(testnet.http_endpoint.clone())
858            .get_max_priority_fee_per_gas()
859            .await
860            .unwrap();
861        assert_eq!(expected_fee_per_gas, u128::from(fee_per_gas));
862    }
863
864    #[tokio::test]
865    async fn test_gas_price() {
866        let testnet = start_default_anvil_testnet(false).await;
867        let provider = get_provider_http(testnet.http_endpoint.clone());
868
869        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
870            .await
871            .unwrap();
872        let gas_price = instrumented_client.suggest_gas_price().await.unwrap();
873        let expected_gas_price = provider.clone().get_gas_price().await.unwrap();
874        assert_eq!(u128::from(gas_price), expected_gas_price);
875    }
876
877    #[tokio::test]
878    async fn test_sync_status() {
879        let testnet = start_default_anvil_testnet(false).await;
880        let provider = get_provider_http(testnet.http_endpoint.clone());
881
882        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
883            .await
884            .unwrap();
885        let sync_status = instrumented_client.sync_progress().await.unwrap();
886        let expected_sync_status = provider.clone().syncing().await.unwrap();
887        assert_eq!(expected_sync_status, sync_status);
888    }
889
890    #[tokio::test]
891    async fn test_chain_id() {
892        let testnet = start_default_anvil_testnet(false).await;
893        let provider = get_provider_http(testnet.http_endpoint.clone());
894
895        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
896            .await
897            .unwrap();
898
899        let expected_chain_id = provider.clone().get_chain_id().await.unwrap();
900        let chain_id = instrumented_client.chain_id().await.unwrap();
901
902        assert_eq!(expected_chain_id, chain_id);
903    }
904
905    #[tokio::test]
906    async fn test_balance_at() {
907        let testnet = start_default_anvil_testnet(false).await;
908        let provider = get_provider_http(testnet.http_endpoint.clone());
909
910        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
911            .await
912            .unwrap();
913        let address = provider.get_accounts().await.unwrap()[0];
914
915        let expected_balance_at = provider.get_balance(address).await.unwrap();
916        let balance_at = instrumented_client
917            .balance_at(address, BlockNumberOrTag::Latest)
918            .await
919            .unwrap();
920
921        assert_eq!(expected_balance_at, balance_at);
922    }
923
924    #[tokio::test]
925    async fn test_subscribe_new_head() {
926        let testnet = start_default_anvil_testnet(false).await;
927
928        let instrumented_client = InstrumentedClient::new_ws(testnet.ws_endpoint)
929            .await
930            .unwrap();
931        let subscription: TransportResult<Subscription<SubscriptionResult>> =
932            instrumented_client.subscribe_new_head().await;
933        assert!(subscription.is_ok());
934    }
935
936    #[tokio::test]
937    async fn test_subscribe_filter_logs() {
938        let testnet = start_default_anvil_testnet(false).await;
939        let provider = get_provider_http(testnet.http_endpoint.clone());
940
941        let instrumented_client = InstrumentedClient::new_ws(testnet.ws_endpoint)
942            .await
943            .unwrap();
944        let address = provider.clone().get_accounts().await.unwrap()[0];
945        let filter = Filter::new().address(address.to_string().parse::<Address>().unwrap());
946
947        let subscription: TransportResult<Subscription<SubscriptionResult>> =
948            instrumented_client.subscribe_filter_logs(filter).await;
949
950        assert!(subscription.is_ok());
951    }
952
953    #[tokio::test]
954    async fn test_block_by_hash() {
955        let testnet = start_default_anvil_testnet(false).await;
956        let provider = get_provider_http(testnet.http_endpoint.clone());
957
958        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
959            .await
960            .unwrap();
961
962        // get the hash from the last block
963        let hash = provider
964            .get_block(BlockId::latest())
965            .await
966            .unwrap()
967            .unwrap()
968            .header
969            .hash;
970
971        let expected_block = provider.get_block_by_hash(hash).full().await.unwrap();
972        let block = instrumented_client.block_by_hash(hash).await.unwrap();
973
974        assert_eq!(expected_block, block);
975    }
976
977    #[tokio::test]
978    async fn test_block_by_number() {
979        let testnet = start_default_anvil_testnet(false).await;
980        let provider = get_provider_http(testnet.http_endpoint.clone());
981
982        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
983            .await
984            .unwrap();
985        let block_number = 1;
986
987        let expected_block = provider
988            .get_block_by_number(block_number.into())
989            .full()
990            .await
991            .unwrap();
992        let block = instrumented_client
993            .block_by_number(block_number.into())
994            .await
995            .unwrap();
996
997        assert_eq!(expected_block, block);
998    }
999
1000    #[tokio::test]
1001    async fn test_transaction_count() {
1002        let testnet = start_default_anvil_testnet(false).await;
1003        let provider = get_provider_http(testnet.http_endpoint.clone());
1004
1005        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1006            .await
1007            .unwrap();
1008
1009        let block = provider
1010            .get_block(BlockId::latest())
1011            .await
1012            .unwrap()
1013            .unwrap();
1014
1015        let block_hash = block.header.hash;
1016        let tx_count = instrumented_client
1017            .transaction_count(B256::from_slice(block_hash.as_slice()))
1018            .await
1019            .unwrap();
1020        let expected_tx_count = block.transactions.len();
1021
1022        assert_eq!(tx_count, expected_tx_count as u64);
1023    }
1024
1025    /// This test tests the following methods
1026    /// * `send_transaction`
1027    /// * `transaction_by_hash`
1028    /// * `transaction_receipt`
1029    /// * `transaction_in_block`
1030    #[tokio::test]
1031    #[ignore]
1032    async fn test_transaction_methods() {
1033        let testnet = start_default_anvil_testnet(false).await;
1034        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1035            .await
1036            .unwrap();
1037
1038        // build the transaction
1039        let to = address!("a0Ee7A142d267C1f36714E4a8F75612F20a79720");
1040        let mut tx = TxLegacy {
1041            to: Call(to),
1042            value: U256::from(0),
1043            gas_limit: 2_000_000,
1044            nonce: 0x69, // nonce queried from the sender account
1045            gas_price: 21_000_000_000,
1046            input: bytes!(),
1047            chain_id: Some(31337),
1048        };
1049
1050        let private_key_hex =
1051            "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string();
1052        let signer: PrivateKeySigner = private_key_hex.parse().unwrap();
1053        let signature = signer.sign_transaction_sync(&mut tx).unwrap();
1054        let signed_tx = tx.into_signed(signature);
1055        let tx: TxEnvelope = TxEnvelope::from(signed_tx);
1056
1057        // test send_transaction
1058        let tx_hash = instrumented_client.send_transaction(tx).await;
1059        assert!(tx_hash.is_ok());
1060        let tx_hash = B256::from_slice(tx_hash.unwrap().as_ref());
1061
1062        // test transaction_by_hash
1063        let tx_by_hash = instrumented_client.transaction_by_hash(tx_hash).await;
1064        assert!(tx_by_hash.is_ok());
1065
1066        wait_transaction(testnet.http_endpoint.clone(), tx_hash)
1067            .await
1068            .unwrap();
1069
1070        // test transaction_receipt
1071        let receipt = instrumented_client.transaction_receipt(tx_hash).await;
1072        assert!(receipt.is_ok());
1073        let receipt = receipt.unwrap();
1074
1075        // test transaction_in_block
1076        let tx_by_block = instrumented_client
1077            .transaction_in_block(
1078                receipt.block_hash.unwrap(),
1079                receipt.transaction_index.unwrap(),
1080            )
1081            .await;
1082        assert!(tx_by_block.is_ok());
1083    }
1084
1085    #[tokio::test]
1086    async fn test_estimate_gas() {
1087        let testnet = start_default_anvil_testnet(false).await;
1088        let provider = get_provider_http(testnet.http_endpoint.clone());
1089
1090        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1091            .await
1092            .unwrap();
1093        let accounts = provider.get_accounts().await.unwrap();
1094        let from = accounts.first().unwrap();
1095        let to = accounts.get(1).unwrap();
1096
1097        // build the transaction
1098        let tx = TxLegacy {
1099            to: Call(*to),
1100            value: U256::from(0),
1101            gas_limit: 2_000_000,
1102            nonce: 0,
1103            gas_price: 1_000_000,
1104            input: bytes!(),
1105            chain_id: Some(31337),
1106        };
1107        let tx_request: TransactionRequest = tx.clone().into();
1108        let tx_request = tx_request.from(*from);
1109
1110        let expected_estimated_gas = provider
1111            .clone()
1112            .estimate_gas(tx_request.clone())
1113            .await
1114            .unwrap();
1115        let estimated_gas = instrumented_client.estimate_gas(tx_request).await.unwrap();
1116        assert_eq!(expected_estimated_gas, estimated_gas);
1117    }
1118
1119    #[tokio::test]
1120    async fn test_call_contract_and_pending_call_contract() {
1121        let testnet = start_default_anvil_testnet(false).await;
1122        let provider = get_provider_http(testnet.http_endpoint.clone());
1123
1124        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1125            .await
1126            .unwrap();
1127
1128        let anvil = provider.clone();
1129        let accounts = anvil.get_accounts().await.unwrap();
1130        let from = accounts.first().unwrap();
1131        let to = accounts.get(1).unwrap();
1132
1133        let nonce = instrumented_client
1134            .nonce_at(*from, BlockNumberOrTag::Latest)
1135            .await
1136            .unwrap();
1137
1138        // build the transaction
1139        let tx = TxLegacy {
1140            to: Call(*to),
1141            value: U256::from(0),
1142            gas_limit: 1_000_000,
1143            nonce,
1144            gas_price: 21_000_000_000,
1145            input: bytes!(),
1146            chain_id: Some(31337),
1147        };
1148        let tx_request: TransactionRequest = tx.clone().into();
1149        let tx_request = tx_request.from(*from);
1150
1151        // test call_contract
1152        let expected_bytes = anvil.call(tx_request.clone()).await.unwrap();
1153        let bytes = instrumented_client
1154            .call_contract(tx_request.clone(), BlockNumberOrTag::Earliest)
1155            .await
1156            .unwrap();
1157        assert_eq!(expected_bytes, bytes);
1158
1159        // test pending_call_contract
1160        let bytes = instrumented_client.pending_call_contract(tx_request).await;
1161        assert!(bytes.is_ok());
1162    }
1163
1164    #[tokio::test]
1165    async fn test_filter_logs() {
1166        let testnet = start_default_anvil_testnet(false).await;
1167        let provider = get_provider_http(testnet.http_endpoint.clone());
1168
1169        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1170            .await
1171            .unwrap();
1172        let address = provider.clone().get_accounts().await.unwrap()[0];
1173        let filter = Filter::new().address(address.to_string().parse::<Address>().unwrap());
1174
1175        let expected_logs = provider.clone().get_logs(&filter).await.unwrap();
1176        let logs = instrumented_client.filter_logs(filter).await.unwrap();
1177
1178        assert_eq!(expected_logs, logs);
1179    }
1180
1181    #[tokio::test]
1182    async fn test_storage_at() {
1183        let testnet = start_default_anvil_testnet(false).await;
1184        let provider = get_provider_http(testnet.http_endpoint.clone());
1185
1186        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1187            .await
1188            .unwrap();
1189
1190        let account = provider.clone().get_accounts().await.unwrap()[0];
1191        let expected_storage = provider
1192            .clone()
1193            .get_storage_at(account, U256::ZERO)
1194            .await
1195            .unwrap();
1196
1197        let storage = instrumented_client
1198            .storage_at(account, U256::ZERO, U256::ZERO)
1199            .await
1200            .unwrap();
1201
1202        assert_eq!(expected_storage, storage);
1203    }
1204
1205    #[tokio::test]
1206    async fn test_block_number() {
1207        let testnet = start_default_anvil_testnet(false).await;
1208        let provider = get_provider_http(testnet.http_endpoint.clone());
1209
1210        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1211            .await
1212            .unwrap();
1213
1214        // Stop auto-mining to avoid flaky test results caused by block updating between block number queries
1215        let _output = testnet
1216            .container
1217            .exec(testcontainers::core::ExecCommand::new([
1218                "cast",
1219                "rpc",
1220                "evm_setAutomine",
1221                "false",
1222            ]))
1223            .await
1224            .expect("Failed to mine anvil blocks");
1225        tokio::time::sleep(blueprint_std::time::Duration::from_secs(5)).await;
1226
1227        let before = provider.clone().get_block_number().await.unwrap();
1228        let block_number = instrumented_client.block_number().await.unwrap();
1229        let after = provider.get_block_number().await.unwrap();
1230
1231        assert!(
1232            block_number >= before && block_number <= after,
1233            "instrumented block number {block_number} not within provider range {before}..={after}"
1234        );
1235    }
1236
1237    #[tokio::test]
1238    async fn test_code_at() {
1239        let testnet = start_default_anvil_testnet(false).await;
1240        let provider = get_provider_http(testnet.http_endpoint.clone());
1241
1242        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1243            .await
1244            .unwrap();
1245        let address = provider.get_accounts().await.unwrap()[0];
1246
1247        let expected_code = provider.get_code_at(address).await.unwrap();
1248        let code = instrumented_client
1249            .code_at(address, BlockNumberOrTag::Latest)
1250            .await
1251            .unwrap();
1252
1253        assert_eq!(expected_code, code);
1254    }
1255
1256    #[tokio::test]
1257    async fn test_fee_history() {
1258        let testnet = start_default_anvil_testnet(false).await;
1259        let provider = get_provider_http(testnet.http_endpoint.clone());
1260
1261        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1262            .await
1263            .unwrap();
1264
1265        // Stop auto-mining to avoid flaky test results caused by block updating between queries
1266        let _output = testnet
1267            .container
1268            .exec(testcontainers::core::ExecCommand::new([
1269                "cast",
1270                "rpc",
1271                "evm_setAutomine",
1272                "false",
1273            ]))
1274            .await
1275            .expect("Failed to mine anvil blocks");
1276        tokio::time::sleep(blueprint_std::time::Duration::from_secs(5)).await;
1277
1278        let block_count = 4;
1279        let last_block = BlockNumberOrTag::Latest;
1280        let reward_percentiles = [0.2, 0.3];
1281
1282        let expected_fee_history = provider
1283            .get_fee_history(block_count, last_block, &reward_percentiles)
1284            .await
1285            .unwrap();
1286        let fee_history = instrumented_client
1287            .fee_history(block_count, last_block, &reward_percentiles)
1288            .await
1289            .unwrap();
1290
1291        assert_eq!(expected_fee_history, fee_history);
1292    }
1293
1294    #[tokio::test]
1295    async fn test_header_by_hash() {
1296        let testnet = start_default_anvil_testnet(false).await;
1297        let provider = get_provider_http(testnet.http_endpoint.clone());
1298
1299        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1300            .await
1301            .unwrap();
1302        let hash = provider
1303            .get_block(BlockId::latest())
1304            .await
1305            .unwrap()
1306            .unwrap()
1307            .header
1308            .hash;
1309        let expected_header = provider
1310            .get_block_by_hash(hash)
1311            .await
1312            .unwrap()
1313            .unwrap()
1314            .header;
1315        let header = instrumented_client.header_by_hash(hash).await.unwrap();
1316
1317        assert_eq!(expected_header, header);
1318    }
1319
1320    #[tokio::test]
1321    async fn test_header_by_number() {
1322        let testnet = start_default_anvil_testnet(false).await;
1323        let provider = get_provider_http(testnet.http_endpoint.clone());
1324
1325        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1326            .await
1327            .unwrap();
1328        let block_number = BlockNumberOrTag::Earliest;
1329
1330        let header = instrumented_client
1331            .header_by_number(block_number)
1332            .await
1333            .unwrap();
1334
1335        let expected_header = provider
1336            .get_block_by_number(block_number)
1337            .await
1338            .unwrap()
1339            .unwrap()
1340            .header;
1341
1342        assert_eq!(expected_header, header);
1343    }
1344
1345    #[tokio::test]
1346    async fn test_nonce_at() {
1347        let testnet = start_default_anvil_testnet(false).await;
1348        let provider = get_provider_http(testnet.http_endpoint.clone());
1349
1350        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1351            .await
1352            .unwrap();
1353        let address = provider.get_accounts().await.unwrap()[0];
1354
1355        let expected_nonce = provider.get_transaction_count(address).await.unwrap();
1356        let nonce = instrumented_client
1357            .nonce_at(address, BlockNumberOrTag::Latest)
1358            .await
1359            .unwrap();
1360
1361        assert_eq!(expected_nonce, nonce);
1362    }
1363
1364    #[tokio::test]
1365    async fn test_pending_balance_at() {
1366        let testnet = start_default_anvil_testnet(false).await;
1367        let provider = get_provider_http(testnet.http_endpoint.clone());
1368
1369        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1370            .await
1371            .unwrap();
1372        let address = provider.get_accounts().await.unwrap()[0];
1373
1374        // TODO: currently comparing "pending" balance with "latest" balance. Check for pending transactions?
1375        let expected_balance = provider.get_balance(address).await.unwrap();
1376        let balance = instrumented_client
1377            .pending_balance_at(address)
1378            .await
1379            .unwrap();
1380
1381        assert_eq!(expected_balance, balance);
1382    }
1383
1384    #[tokio::test]
1385    async fn test_pending_code_at() {
1386        let testnet = start_default_anvil_testnet(false).await;
1387        let provider = get_provider_http(testnet.http_endpoint.clone());
1388
1389        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1390            .await
1391            .unwrap();
1392        let address = provider.get_accounts().await.unwrap()[0];
1393
1394        // TODO: currently comparing "pending" with "latest". Check for pending transactions?
1395        let expected_code = provider.get_code_at(address).await.unwrap();
1396        let code = instrumented_client.pending_code_at(address).await.unwrap();
1397
1398        assert_eq!(expected_code, code);
1399    }
1400
1401    #[tokio::test]
1402    async fn test_pending_nonce_at() {
1403        let testnet = start_default_anvil_testnet(false).await;
1404        let provider = get_provider_http(testnet.http_endpoint.clone());
1405
1406        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1407            .await
1408            .unwrap();
1409        let address = provider.get_accounts().await.unwrap()[0];
1410
1411        // TODO: currently comparing "pending" with "latest". Check for pending transactions?
1412        let expected_pending_nonce_at = provider.get_transaction_count(address).await.unwrap();
1413        let pending_nonce_at = instrumented_client.pending_nonce_at(address).await.unwrap();
1414
1415        assert_eq!(expected_pending_nonce_at, pending_nonce_at);
1416    }
1417
1418    #[tokio::test]
1419    async fn test_pending_storage_at() {
1420        let testnet = start_default_anvil_testnet(false).await;
1421        let provider = get_provider_http(testnet.http_endpoint.clone());
1422
1423        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1424            .await
1425            .unwrap();
1426        let address = provider.get_accounts().await.unwrap()[0];
1427        let key = U256::from(10);
1428
1429        // TODO: currently comparing "pending" with "latest". Check for pending transactions?
1430        // TODO: set storage and check change
1431        let expected_pending_storage_at = provider.get_storage_at(address, key).await.unwrap();
1432        let pending_storage_at = instrumented_client
1433            .pending_storage_at(address, key)
1434            .await
1435            .unwrap();
1436
1437        assert_eq!(expected_pending_storage_at, pending_storage_at);
1438    }
1439
1440    #[tokio::test]
1441    async fn test_pending_transaction_count() {
1442        let testnet = start_default_anvil_testnet(false).await;
1443        let provider = get_provider_http(testnet.http_endpoint.clone());
1444
1445        let instrumented_client = InstrumentedClient::new(testnet.http_endpoint.clone())
1446            .await
1447            .unwrap();
1448
1449        let expected_transaction_count: u64 = provider
1450            .get_block_by_number(BlockNumberOrTag::Pending)
1451            .await
1452            .unwrap()
1453            .unwrap()
1454            .transactions
1455            .len() as u64;
1456
1457        let transaction_count = instrumented_client.pending_transaction_count().await;
1458
1459        assert_eq!(expected_transaction_count, transaction_count.unwrap());
1460    }
1461}