Skip to main content

alloy_provider/layers/
cache.rs

1use crate::{
2    utils, ParamsWithBlock, Provider, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock,
3};
4use alloy_eips::BlockId;
5use alloy_json_rpc::{RpcError, RpcSend};
6use alloy_network::Network;
7use alloy_primitives::{
8    keccak256, Address, Bytes, StorageKey, StorageValue, TxHash, B256, U256, U64,
9};
10use alloy_rpc_types_eth::{
11    BlockNumberOrTag, EIP1186AccountProofResponse, Filter, Log, StorageValuesRequest,
12    StorageValuesResponse,
13};
14use alloy_transport::{TransportErrorKind, TransportResult};
15use lru::LruCache;
16use parking_lot::RwLock;
17use serde::{Deserialize, Serialize};
18use std::{io::BufReader, marker::PhantomData, num::NonZero, path::PathBuf, sync::Arc};
19/// A provider layer that caches RPC responses and serves them on subsequent requests.
20///
21/// In order to initialize the caching layer, the path to the cache file is provided along with the
22/// max number of items that are stored in the in-memory LRU cache.
23///
24/// One can load the cache from the file system by calling `load_cache` and save the cache to the
25/// file system by calling `save_cache`.
26#[derive(Debug, Clone)]
27pub struct CacheLayer {
28    /// In-memory LRU cache, mapping requests to responses.
29    cache: SharedCache,
30}
31
32impl CacheLayer {
33    /// Instantiate a new cache layer with the maximum number of
34    /// items to store.
35    pub fn new(max_items: u32) -> Self {
36        Self { cache: SharedCache::new(max_items) }
37    }
38
39    /// Returns the maximum number of items that can be stored in the cache, set at initialization.
40    pub const fn max_items(&self) -> u32 {
41        self.cache.max_items()
42    }
43
44    /// Returns the shared cache.
45    pub fn cache(&self) -> SharedCache {
46        self.cache.clone()
47    }
48}
49
50impl<P, N> ProviderLayer<P, N> for CacheLayer
51where
52    P: Provider<N>,
53    N: Network,
54{
55    type Provider = CacheProvider<P, N>;
56
57    fn layer(&self, inner: P) -> Self::Provider {
58        CacheProvider::new(inner, self.cache())
59    }
60}
61
62/// The [`CacheProvider`] holds the underlying in-memory LRU cache and overrides methods
63/// from the [`Provider`] trait. It attempts to fetch from the cache and fallbacks to
64/// the RPC in case of a cache miss.
65///
66/// Most importantly, the [`CacheProvider`] adds `save_cache` and `load_cache` methods
67/// to the provider interface, allowing users to save the cache to disk and load it
68/// from there on demand.
69#[derive(Debug, Clone)]
70pub struct CacheProvider<P, N> {
71    /// Inner provider.
72    inner: P,
73    /// In-memory LRU cache, mapping requests to responses.
74    cache: SharedCache,
75    /// Phantom data
76    _pd: PhantomData<N>,
77}
78
79impl<P, N> CacheProvider<P, N>
80where
81    P: Provider<N>,
82    N: Network,
83{
84    /// Instantiate a new cache provider.
85    pub const fn new(inner: P, cache: SharedCache) -> Self {
86        Self { inner, cache, _pd: PhantomData }
87    }
88}
89
90/// Uses underlying transport client to fetch data from the RPC.
91///
92/// This is specific to RPC requests that require the `block_id` parameter.
93///
94/// Fetches from the RPC and saves the response to the cache.
95///
96/// Returns a ProviderCall::BoxedFuture
97macro_rules! rpc_call_with_block {
98    ($cache:expr, $client:expr, $req:expr) => {{
99        let client =
100            $client.upgrade().ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"));
101        let cache = $cache.clone();
102        ProviderCall::BoxedFuture(Box::pin(async move {
103            let client = client?;
104
105            let result = client.request($req.method(), $req.params()).map_params(|params| {
106                ParamsWithBlock::new(params, $req.block_id.unwrap_or(BlockId::latest()))
107            });
108
109            let res = result.await?;
110            // Insert into cache only for deterministic block identifiers (exclude tag-based ids
111            // like latest/pending/earliest). Caching tag-based results can lead to stale data.
112            if !$req.has_block_tag() {
113                let json_str = serde_json::to_string(&res).map_err(TransportErrorKind::custom)?;
114                let hash = $req.params_hash()?;
115                let _ = cache.put(hash, json_str);
116            }
117
118            Ok(res)
119        }))
120    }};
121}
122
123/// Attempts to fetch the response from the cache by using the hash of the request params.
124///
125/// Fetches from the RPC in case of a cache miss
126///
127/// This helps overriding [`Provider`] methods that return `RpcWithBlock`.
128macro_rules! cache_rpc_call_with_block {
129    ($cache:expr, $client:expr, $req:expr) => {{
130        if $req.has_block_tag() {
131            return rpc_call_with_block!($cache, $client, $req);
132        }
133
134        let hash = $req.params_hash().ok();
135
136        if let Some(hash) = hash {
137            if let Ok(Some(cached)) = $cache.get_deserialized(&hash) {
138                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
139            }
140        }
141
142        rpc_call_with_block!($cache, $client, $req)
143    }};
144}
145
146#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
147#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
148impl<P, N> Provider<N> for CacheProvider<P, N>
149where
150    P: Provider<N>,
151    N: Network,
152{
153    #[inline(always)]
154    fn root(&self) -> &RootProvider<N> {
155        self.inner.root()
156    }
157
158    fn get_block_receipts(
159        &self,
160        block: BlockId,
161    ) -> ProviderCall<(BlockId,), Option<Vec<N::ReceiptResponse>>> {
162        let req = RequestType::new("eth_getBlockReceipts", (block,)).with_block_id(block);
163
164        let redirect = req.has_block_tag();
165
166        if !redirect {
167            let params_hash = req.params_hash().ok();
168
169            if let Some(hash) = params_hash {
170                if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
171                    return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
172                }
173            }
174        }
175
176        let client = self.inner.weak_client();
177        let cache = self.cache.clone();
178
179        ProviderCall::BoxedFuture(Box::pin(async move {
180            let client = client
181                .upgrade()
182                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
183
184            let result = client.request(req.method(), req.params()).await?;
185
186            if !redirect {
187                if let Some(ref receipts) = result {
188                    let json_str =
189                        serde_json::to_string(receipts).map_err(TransportErrorKind::custom)?;
190                    let hash = req.params_hash()?;
191                    let _ = cache.put(hash, json_str);
192                }
193            }
194
195            Ok(result)
196        }))
197    }
198
199    fn get_balance(&self, address: Address) -> RpcWithBlock<Address, U256> {
200        let client = self.inner.weak_client();
201        let cache = self.cache.clone();
202        RpcWithBlock::new_provider(move |block_id| {
203            let req = RequestType::new("eth_getBalance", address).with_block_id(block_id);
204            cache_rpc_call_with_block!(cache, client, req)
205        })
206    }
207
208    fn get_code_at(&self, address: Address) -> RpcWithBlock<Address, Bytes> {
209        let client = self.inner.weak_client();
210        let cache = self.cache.clone();
211        RpcWithBlock::new_provider(move |block_id| {
212            let req = RequestType::new("eth_getCode", address).with_block_id(block_id);
213            cache_rpc_call_with_block!(cache, client, req)
214        })
215    }
216
217    async fn get_logs(&self, filter: &Filter) -> TransportResult<Vec<Log>> {
218        if filter.block_option.as_block_hash().is_none() {
219            // if block options have dynamic range we can't cache them
220            let from_is_number = filter
221                .block_option
222                .get_from_block()
223                .as_ref()
224                .is_some_and(|block| block.is_number());
225            let to_is_number =
226                filter.block_option.get_to_block().as_ref().is_some_and(|block| block.is_number());
227
228            if !from_is_number || !to_is_number {
229                return self.inner.get_logs(filter).await;
230            }
231        }
232
233        let req = RequestType::new("eth_getLogs", (filter,));
234
235        let params_hash = req.params_hash().ok();
236
237        if let Some(hash) = params_hash {
238            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
239                return Ok(cached);
240            }
241        }
242
243        let result = self.inner.get_logs(filter).await?;
244
245        let json_str = serde_json::to_string(&result).map_err(TransportErrorKind::custom)?;
246
247        let hash = req.params_hash()?;
248        let _ = self.cache.put(hash, json_str);
249
250        Ok(result)
251    }
252
253    fn get_proof(
254        &self,
255        address: Address,
256        keys: Vec<StorageKey>,
257    ) -> RpcWithBlock<(Address, Vec<StorageKey>), EIP1186AccountProofResponse> {
258        let client = self.inner.weak_client();
259        let cache = self.cache.clone();
260        RpcWithBlock::new_provider(move |block_id| {
261            let req =
262                RequestType::new("eth_getProof", (address, keys.clone())).with_block_id(block_id);
263            cache_rpc_call_with_block!(cache, client, req)
264        })
265    }
266
267    fn get_storage_at(
268        &self,
269        address: Address,
270        key: U256,
271    ) -> RpcWithBlock<(Address, U256), StorageValue> {
272        let client = self.inner.weak_client();
273        let cache = self.cache.clone();
274        RpcWithBlock::new_provider(move |block_id| {
275            let req = RequestType::new("eth_getStorageAt", (address, key)).with_block_id(block_id);
276            cache_rpc_call_with_block!(cache, client, req)
277        })
278    }
279
280    fn get_storage_values(
281        &self,
282        requests: StorageValuesRequest,
283    ) -> RpcWithBlock<(StorageValuesRequest,), StorageValuesResponse> {
284        let client = self.inner.weak_client();
285        let cache = self.cache.clone();
286        RpcWithBlock::new_provider(move |block_id| {
287            let req = RequestType::new("eth_getStorageValues", (requests.clone(),))
288                .with_block_id(block_id);
289            cache_rpc_call_with_block!(cache, client, req)
290        })
291    }
292
293    fn get_transaction_by_hash(
294        &self,
295        hash: TxHash,
296    ) -> ProviderCall<(TxHash,), Option<N::TransactionResponse>> {
297        let req = RequestType::new("eth_getTransactionByHash", (hash,));
298
299        let params_hash = req.params_hash().ok();
300
301        if let Some(hash) = params_hash {
302            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
303                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
304            }
305        }
306        let client = self.inner.weak_client();
307        let cache = self.cache.clone();
308        ProviderCall::BoxedFuture(Box::pin(async move {
309            let client = client
310                .upgrade()
311                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
312            let result = client.request(req.method(), req.params()).await?;
313
314            if let Some(ref tx) = result {
315                let json_str = serde_json::to_string(tx).map_err(TransportErrorKind::custom)?;
316                let hash = req.params_hash()?;
317                let _ = cache.put(hash, json_str);
318            }
319
320            Ok(result)
321        }))
322    }
323
324    fn get_raw_transaction_by_hash(&self, hash: TxHash) -> ProviderCall<(TxHash,), Option<Bytes>> {
325        let req = RequestType::new("eth_getRawTransactionByHash", (hash,));
326
327        let params_hash = req.params_hash().ok();
328
329        if let Some(hash) = params_hash {
330            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
331                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
332            }
333        }
334
335        let client = self.inner.weak_client();
336        let cache = self.cache.clone();
337        ProviderCall::BoxedFuture(Box::pin(async move {
338            let client = client
339                .upgrade()
340                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
341
342            let result = client.request(req.method(), req.params()).await?;
343
344            if let Some(ref tx) = result {
345                let json_str = serde_json::to_string(tx).map_err(TransportErrorKind::custom)?;
346                let hash = req.params_hash()?;
347                let _ = cache.put(hash, json_str);
348            }
349
350            Ok(result)
351        }))
352    }
353
354    fn get_transaction_receipt(
355        &self,
356        hash: TxHash,
357    ) -> ProviderCall<(TxHash,), Option<N::ReceiptResponse>> {
358        let req = RequestType::new("eth_getTransactionReceipt", (hash,));
359
360        let params_hash = req.params_hash().ok();
361
362        if let Some(hash) = params_hash {
363            if let Ok(Some(cached)) = self.cache.get_deserialized(&hash) {
364                return ProviderCall::BoxedFuture(Box::pin(async move { Ok(cached) }));
365            }
366        }
367
368        let client = self.inner.weak_client();
369        let cache = self.cache.clone();
370        ProviderCall::BoxedFuture(Box::pin(async move {
371            let client = client
372                .upgrade()
373                .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
374
375            let result = client.request(req.method(), req.params()).await?;
376
377            if let Some(ref receipt) = result {
378                let json_str =
379                    serde_json::to_string(receipt).map_err(TransportErrorKind::custom)?;
380                let hash = req.params_hash()?;
381                let _ = cache.put(hash, json_str);
382            }
383
384            Ok(result)
385        }))
386    }
387
388    fn get_transaction_count(
389        &self,
390        address: Address,
391    ) -> RpcWithBlock<Address, U64, u64, fn(U64) -> u64> {
392        let client = self.inner.weak_client();
393        let cache = self.cache.clone();
394        RpcWithBlock::new_provider(move |block_id| {
395            let req = RequestType::new("eth_getTransactionCount", address).with_block_id(block_id);
396
397            let redirect = req.has_block_tag();
398
399            if !redirect {
400                let params_hash = req.params_hash().ok();
401
402                if let Some(hash) = params_hash {
403                    if let Ok(Some(cached)) = cache.get_deserialized::<U64>(&hash) {
404                        return ProviderCall::BoxedFuture(Box::pin(async move {
405                            Ok(utils::convert_u64(cached))
406                        }));
407                    }
408                }
409            }
410
411            let client = client.clone();
412            let cache = cache.clone();
413
414            ProviderCall::BoxedFuture(Box::pin(async move {
415                let client = client
416                    .upgrade()
417                    .ok_or_else(|| TransportErrorKind::custom_str("RPC client dropped"))?;
418
419                let result: U64 = client
420                    .request(req.method(), req.params())
421                    .map_params(|params| ParamsWithBlock::new(params, block_id))
422                    .await?;
423
424                if !redirect {
425                    let json_str =
426                        serde_json::to_string(&result).map_err(TransportErrorKind::custom)?;
427                    let hash = req.params_hash()?;
428                    let _ = cache.put(hash, json_str);
429                }
430
431                Ok(utils::convert_u64(result))
432            }))
433        })
434    }
435}
436
437/// Internal type to handle different types of requests and generating their param hashes.
438struct RequestType<Params: RpcSend> {
439    method: &'static str,
440    params: Params,
441    block_id: Option<BlockId>,
442}
443
444impl<Params: RpcSend> RequestType<Params> {
445    const fn new(method: &'static str, params: Params) -> Self {
446        Self { method, params, block_id: None }
447    }
448
449    const fn with_block_id(mut self, block_id: BlockId) -> Self {
450        self.block_id = Some(block_id);
451        self
452    }
453
454    fn params_hash(&self) -> TransportResult<B256> {
455        // Merge the block_id + method + params and hash them.
456        // Ignoring all other BlockIds than BlockId::Hash and
457        // BlockId::Number(BlockNumberOrTag::Number(_)).
458        let hash = serde_json::to_string(&self.params())
459            .map(|p| {
460                keccak256(
461                    match self.block_id {
462                        Some(BlockId::Hash(rpc_block_hash)) => {
463                            format!("{}{}{}", rpc_block_hash, self.method(), p)
464                        }
465                        Some(BlockId::Number(BlockNumberOrTag::Number(number))) => {
466                            format!("{}{}{}", number, self.method(), p)
467                        }
468                        _ => format!("{}{}", self.method(), p),
469                    }
470                    .as_bytes(),
471                )
472            })
473            .map_err(RpcError::ser_err)?;
474
475        Ok(hash)
476    }
477
478    const fn method(&self) -> &'static str {
479        self.method
480    }
481
482    fn params(&self) -> Params {
483        self.params.clone()
484    }
485
486    /// Returns true if the BlockId has been set to a tag value such as "latest", "earliest", or
487    /// "pending".
488    const fn has_block_tag(&self) -> bool {
489        if let Some(block_id) = self.block_id {
490            return !matches!(
491                block_id,
492                BlockId::Hash(_) | BlockId::Number(BlockNumberOrTag::Number(_))
493            );
494        }
495        // Treat absence of BlockId as tag-based (e.g., 'latest'), which is non-deterministic
496        // and should not be cached.
497        true
498    }
499}
500
501#[derive(Debug, Serialize, Deserialize)]
502struct FsCacheEntry {
503    /// Hash of the request params
504    key: B256,
505    /// Serialized response to the request from which the hash was computed.
506    value: String,
507}
508
509/// Shareable cache.
510#[derive(Debug, Clone)]
511pub struct SharedCache {
512    inner: Arc<RwLock<LruCache<B256, String, alloy_primitives::map::FbBuildHasher<32>>>>,
513    max_items: NonZero<usize>,
514}
515
516impl SharedCache {
517    /// Instantiate a new shared cache.
518    pub fn new(max_items: u32) -> Self {
519        let max_items = NonZero::new(max_items as usize).unwrap_or(NonZero::<usize>::MIN);
520        let inner = Arc::new(RwLock::new(LruCache::with_hasher(max_items, Default::default())));
521        Self { inner, max_items }
522    }
523
524    /// Maximum number of items that can be stored in the cache.
525    pub const fn max_items(&self) -> u32 {
526        self.max_items.get() as u32
527    }
528
529    /// Puts a value into the cache, and returns the old value if it existed.
530    pub fn put(&self, key: B256, value: String) -> TransportResult<bool> {
531        Ok(self.inner.write().put(key, value).is_some())
532    }
533
534    /// Gets a value from the cache, if it exists.
535    pub fn get(&self, key: &B256) -> Option<String> {
536        // Need to acquire a write guard to change the order of keys in LRU cache.
537        self.inner.write().get(key).cloned()
538    }
539
540    /// Get deserialized value from the cache.
541    pub fn get_deserialized<T>(&self, key: &B256) -> TransportResult<Option<T>>
542    where
543        T: for<'de> Deserialize<'de>,
544    {
545        let Some(cached) = self.get(key) else { return Ok(None) };
546        let result = serde_json::from_str(&cached).map_err(TransportErrorKind::custom)?;
547        Ok(Some(result))
548    }
549
550    /// Saves the cache to a file specified by the path.
551    /// If the files does not exist, it creates one.
552    /// If the file exists, it overwrites it.
553    pub fn save_cache(&self, path: PathBuf) -> TransportResult<()> {
554        let entries: Vec<FsCacheEntry> = {
555            self.inner
556                .read()
557                .iter()
558                .map(|(key, value)| FsCacheEntry { key: *key, value: value.clone() })
559                .collect()
560        };
561        let file = std::fs::File::create(path).map_err(TransportErrorKind::custom)?;
562        serde_json::to_writer(file, &entries).map_err(TransportErrorKind::custom)?;
563        Ok(())
564    }
565
566    /// Loads the cache from a file specified by the path.
567    /// If the file does not exist, it returns without error.
568    pub fn load_cache(&self, path: PathBuf) -> TransportResult<()> {
569        if !path.exists() {
570            return Ok(());
571        };
572        let file = std::fs::File::open(path).map_err(TransportErrorKind::custom)?;
573        let file = BufReader::new(file);
574        let entries: Vec<FsCacheEntry> =
575            serde_json::from_reader(file).map_err(TransportErrorKind::custom)?;
576        let mut cache = self.inner.write();
577        for entry in entries {
578            cache.put(entry.key, entry.value);
579        }
580
581        Ok(())
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use crate::ProviderBuilder;
589    use alloy_network::TransactionBuilder;
590    use alloy_node_bindings::{utils::run_with_tempdir, Anvil};
591    use alloy_primitives::{b256, bytes, hex, utils::Unit, Bytes, FixedBytes};
592    use alloy_rpc_types_eth::{BlockId, Transaction, TransactionReceipt, TransactionRequest};
593    use alloy_transport::mock::Asserter;
594
595    #[tokio::test]
596    async fn test_get_proof() {
597        run_with_tempdir("get-proof", |dir| async move {
598            let cache_layer = CacheLayer::new(100);
599            let shared_cache = cache_layer.cache();
600            let anvil = Anvil::new().block_time_f64(0.3).spawn();
601            let provider = ProviderBuilder::new().layer(cache_layer).connect_http(anvil.endpoint_url());
602
603            let from = anvil.addresses()[0];
604            let path = dir.join("rpc-cache-proof.txt");
605
606            shared_cache.load_cache(path.clone()).unwrap();
607
608            let calldata: Bytes = "0x6080604052348015600f57600080fd5b506101f28061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80633fb5c1cb146100465780638381f58a14610062578063d09de08a14610080575b600080fd5b610060600480360381019061005b91906100ee565b61008a565b005b61006a610094565b604051610077919061012a565b60405180910390f35b61008861009a565b005b8060008190555050565b60005481565b6000808154809291906100ac90610174565b9190505550565b600080fd5b6000819050919050565b6100cb816100b8565b81146100d657600080fd5b50565b6000813590506100e8816100c2565b92915050565b600060208284031215610104576101036100b3565b5b6000610112848285016100d9565b91505092915050565b610124816100b8565b82525050565b600060208201905061013f600083018461011b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061017f826100b8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101b1576101b0610145565b5b60018201905091905056fea264697066735822122067ac0f21f648b0cacd1b7260772852ad4a0f63e2cc174168c51a6887fd5197a964736f6c634300081a0033".parse().unwrap();
609
610            let tx = TransactionRequest::default()
611                .with_from(from)
612                .with_input(calldata)
613                .with_max_fee_per_gas(1_000_000_000)
614                .with_max_priority_fee_per_gas(1_000_000)
615                .with_gas_limit(1_000_000)
616                .with_nonce(0);
617
618            let tx_receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
619
620            let counter_addr = tx_receipt.contract_address.unwrap();
621
622            let keys = vec![
623                FixedBytes::with_last_byte(0),
624                FixedBytes::with_last_byte(0x1),
625                FixedBytes::with_last_byte(0x2),
626                FixedBytes::with_last_byte(0x3),
627                FixedBytes::with_last_byte(0x4),
628            ];
629
630            let proof =
631                provider.get_proof(counter_addr, keys.clone()).block_id(1.into()).await.unwrap();
632            let proof2 = provider.get_proof(counter_addr, keys).block_id(1.into()).await.unwrap();
633
634            assert_eq!(proof, proof2);
635
636            shared_cache.save_cache(path).unwrap();
637        }).await;
638    }
639
640    #[tokio::test]
641    async fn test_get_tx_by_hash_and_receipt() {
642        run_with_tempdir("get-tx-by-hash", |dir| async move {
643            let cache_layer = CacheLayer::new(100);
644            let shared_cache = cache_layer.cache();
645            let anvil = Anvil::new().block_time_f64(0.3).spawn();
646            let provider = ProviderBuilder::new()
647                .disable_recommended_fillers()
648                .layer(cache_layer)
649                .connect_http(anvil.endpoint_url());
650
651            let path = dir.join("rpc-cache-tx.txt");
652            shared_cache.load_cache(path.clone()).unwrap();
653
654            let req = TransactionRequest::default()
655                .from(anvil.addresses()[0])
656                .to(Address::repeat_byte(5))
657                .value(U256::ZERO)
658                .input(bytes!("deadbeef").into());
659
660            let tx_hash =
661                *provider.send_transaction(req).await.expect("failed to send tx").tx_hash();
662
663            let tx = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from RPC.
664            let tx2 = provider.get_transaction_by_hash(tx_hash).await.unwrap(); // Received from cache.
665            assert_eq!(tx, tx2);
666
667            let receipt = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from RPC.
668            let receipt2 = provider.get_transaction_receipt(tx_hash).await.unwrap(); // Received from cache.
669
670            assert_eq!(receipt, receipt2);
671
672            shared_cache.save_cache(path).unwrap();
673        })
674        .await;
675    }
676
677    #[tokio::test]
678    async fn test_get_transaction_by_hash_retries_after_none() {
679        let cache_layer = CacheLayer::new(100);
680        let shared_cache = cache_layer.cache();
681        let asserter = Asserter::new();
682        let provider = ProviderBuilder::new()
683            .disable_recommended_fillers()
684            .layer(cache_layer)
685            .connect_mocked_client(asserter.clone());
686
687        let tx_hash = b256!("018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f");
688        let req = RequestType::new("eth_getTransactionByHash", (tx_hash,));
689        let cache_key = req.params_hash().unwrap();
690
691        let tx: Transaction = serde_json::from_str(
692            r#"{"hash":"0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f","nonce":"0x1","blockHash":"0x6e4e53d1de650d5a5ebed19b38321db369ef1dc357904284ecf4d89b8834969c","blockNumber":"0x2","transactionIndex":"0x0","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gasPrice":"0x3a29f0f8","gas":"0x1c9c380","maxFeePerGas":"0xba43b7400","maxPriorityFeePerGas":"0x5f5e100","input":"0xd09de08a","r":"0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7","s":"0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319","v":"0x0","yParity":"0x0","chainId":"0x7a69","accessList":[],"type":"0x2"}"#,
693        )
694        .unwrap();
695
696        asserter.push_success(&Option::<Transaction>::None);
697        asserter.push_success(&Some(tx.clone()));
698
699        let first = provider.get_transaction_by_hash(tx_hash).await.unwrap();
700        assert_eq!(first, None);
701        assert!(shared_cache.get(&cache_key).is_none());
702
703        let second = provider.get_transaction_by_hash(tx_hash).await.unwrap();
704        assert_eq!(second, Some(tx));
705        assert!(shared_cache.get(&cache_key).is_some());
706    }
707
708    #[tokio::test]
709    async fn test_get_raw_transaction_by_hash_retries_after_none() {
710        let cache_layer = CacheLayer::new(100);
711        let shared_cache = cache_layer.cache();
712        let asserter = Asserter::new();
713        let provider = ProviderBuilder::new()
714            .disable_recommended_fillers()
715            .layer(cache_layer)
716            .connect_mocked_client(asserter.clone());
717
718        let tx_hash = TxHash::with_last_byte(1);
719        let req = RequestType::new("eth_getRawTransactionByHash", (tx_hash,));
720        let cache_key = req.params_hash().unwrap();
721        let raw_tx = bytes!("deadbeef");
722
723        asserter.push_success(&Option::<Bytes>::None);
724        asserter.push_success(&Some(raw_tx.clone()));
725
726        let first = provider.get_raw_transaction_by_hash(tx_hash).await.unwrap();
727        assert_eq!(first, None);
728        assert!(shared_cache.get(&cache_key).is_none());
729
730        let second = provider.get_raw_transaction_by_hash(tx_hash).await.unwrap();
731        assert_eq!(second, Some(raw_tx));
732        assert!(shared_cache.get(&cache_key).is_some());
733    }
734
735    #[tokio::test]
736    async fn test_get_transaction_receipt_retries_after_none() {
737        let cache_layer = CacheLayer::new(100);
738        let shared_cache = cache_layer.cache();
739        let asserter = Asserter::new();
740        let provider = ProviderBuilder::new()
741            .disable_recommended_fillers()
742            .layer(cache_layer)
743            .connect_mocked_client(asserter.clone());
744
745        let tx_hash = b256!("ea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f");
746        let req = RequestType::new("eth_getTransactionReceipt", (tx_hash,));
747        let cache_key = req.params_hash().unwrap();
748
749        let receipt: TransactionReceipt = serde_json::from_str(
750            r#"{
751                "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f",
752                "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
753                "blockNumber": "0xf4240",
754                "logsBloom": "0x00000000000000000000000000000000000800000000000000000000000800000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000",
755                "gasUsed": "0x723c",
756                "root": "0x284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10",
757                "contractAddress": null,
758                "cumulativeGasUsed": "0x723c",
759                "transactionIndex": "0x0",
760                "from": "0x39fa8c5f2793459d6622857e7d9fbb4bd91766d3",
761                "to": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
762                "type": "0x0",
763                "effectiveGasPrice": "0x12bfb19e60",
764                "logs": [
765                    {
766                        "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
767                        "address": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
768                        "logIndex": "0x0",
769                        "data": "0x00000000000000000000000039fa8c5f2793459d6622857e7d9fbb4bd91766d30000000000000000000000000000000000000000000000056bc75e2d63100000",
770                        "removed": false,
771                        "topics": [
772                            "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
773                        ],
774                        "blockNumber": "0xf4240",
775                        "transactionIndex": "0x0",
776                        "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f"
777                    }
778                ]
779            }"#,
780        )
781        .unwrap();
782
783        asserter.push_success(&Option::<TransactionReceipt>::None);
784        asserter.push_success(&Some(receipt.clone()));
785
786        let first = provider.get_transaction_receipt(tx_hash).await.unwrap();
787        assert_eq!(first, None);
788        assert!(shared_cache.get(&cache_key).is_none());
789
790        let second = provider.get_transaction_receipt(tx_hash).await.unwrap();
791        assert_eq!(second, Some(receipt));
792        assert!(shared_cache.get(&cache_key).is_some());
793    }
794
795    #[tokio::test]
796    async fn test_block_receipts() {
797        run_with_tempdir("get-block-receipts", |dir| async move {
798            let cache_layer = CacheLayer::new(100);
799            let shared_cache = cache_layer.cache();
800            let anvil = Anvil::new().spawn();
801            let provider = ProviderBuilder::new().layer(cache_layer).connect_http(anvil.endpoint_url());
802
803            let path = dir.join("rpc-cache-block-receipts.txt");
804            shared_cache.load_cache(path.clone()).unwrap();
805
806            // Send txs
807
808            let receipt = provider
809                    .send_raw_transaction(
810                        // Transfer 1 ETH from default EOA address to the Genesis address.
811                        bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b").as_ref()
812                    )
813                    .await.unwrap().get_receipt().await.unwrap();
814
815            let block_number = receipt.block_number.unwrap();
816
817            let receipts =
818                provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from RPC.
819            let receipts2 =
820                provider.get_block_receipts(block_number.into()).await.unwrap(); // Received from cache.
821            assert_eq!(receipts, receipts2);
822
823            assert!(receipts.is_some_and(|r| r[0] == receipt));
824
825            shared_cache.save_cache(path).unwrap();
826        })
827        .await
828    }
829
830    #[tokio::test]
831    async fn test_get_balance() {
832        run_with_tempdir("get-balance", |dir| async move {
833            let cache_layer = CacheLayer::new(100);
834            let cache_layer2 = cache_layer.clone();
835            let shared_cache = cache_layer.cache();
836            let anvil = Anvil::new().spawn();
837            let provider = ProviderBuilder::new()
838                .disable_recommended_fillers()
839                .layer(cache_layer)
840                .connect_http(anvil.endpoint_url());
841
842            let path = dir.join("rpc-cache-balance.txt");
843            shared_cache.load_cache(path.clone()).unwrap();
844
845            let to = Address::repeat_byte(5);
846
847            // Send a transaction to change balance
848            let req = TransactionRequest::default()
849                .from(anvil.addresses()[0])
850                .to(to)
851                .value(Unit::ETHER.wei());
852
853            let receipt = provider
854                .send_transaction(req)
855                .await
856                .expect("failed to send tx")
857                .get_receipt()
858                .await
859                .unwrap();
860            let block_number = receipt.block_number.unwrap();
861
862            // Get balance from RPC (populates cache)
863            let balance = provider.get_balance(to).block_id(block_number.into()).await.unwrap();
864            assert_eq!(balance, Unit::ETHER.wei());
865
866            // Drop anvil to ensure second call can't hit RPC
867            drop(anvil);
868
869            // Create new provider with same cache but dead endpoint
870            let provider2 = ProviderBuilder::new()
871                .disable_recommended_fillers()
872                .layer(cache_layer2)
873                .connect_http("http://localhost:1".parse().unwrap());
874
875            // This only succeeds if cache is hit
876            let balance2 = provider2.get_balance(to).block_id(block_number.into()).await.unwrap();
877            assert_eq!(balance, balance2);
878
879            shared_cache.save_cache(path).unwrap();
880        })
881        .await;
882    }
883
884    #[tokio::test]
885    async fn test_get_code() {
886        run_with_tempdir("get-code", |dir| async move {
887            let cache_layer = CacheLayer::new(100);
888            let shared_cache = cache_layer.cache();
889            let provider = ProviderBuilder::new().disable_recommended_fillers().with_gas_estimation().layer(cache_layer).connect_anvil_with_wallet();
890
891            let path = dir.join("rpc-cache-code.txt");
892            shared_cache.load_cache(path.clone()).unwrap();
893
894            let bytecode = hex::decode(
895                // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
896                "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033"
897            ).unwrap();
898            let tx = TransactionRequest::default().with_nonce(0).with_deploy_code(bytecode).with_chain_id(31337);
899
900            let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
901
902            let counter_addr = receipt.contract_address.unwrap();
903
904            let block_id = BlockId::number(receipt.block_number.unwrap());
905
906            let code = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from RPC.
907            let code2 = provider.get_code_at(counter_addr).block_id(block_id).await.unwrap(); // Received from cache.
908            assert_eq!(code, code2);
909
910            shared_cache.save_cache(path).unwrap();
911        })
912        .await;
913    }
914
915    #[cfg(all(test, feature = "anvil-api"))]
916    #[tokio::test]
917    async fn test_get_storage_at_different_block_ids() {
918        use crate::ext::AnvilApi;
919
920        run_with_tempdir("get-code-different-block-id", |dir| async move {
921            let cache_layer = CacheLayer::new(100);
922            let shared_cache = cache_layer.cache();
923            let provider = ProviderBuilder::new().disable_recommended_fillers().with_gas_estimation().layer(cache_layer).connect_anvil_with_wallet();
924
925            let path = dir.join("rpc-cache-code.txt");
926            shared_cache.load_cache(path.clone()).unwrap();
927
928            let bytecode = hex::decode(
929                // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
930                "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033"
931            ).unwrap();
932
933            let tx = TransactionRequest::default().with_nonce(0).with_deploy_code(bytecode).with_chain_id(31337);
934            let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
935            let counter_addr = receipt.contract_address.unwrap();
936            let block_id = BlockId::number(receipt.block_number.unwrap());
937
938            let counter = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id).await.unwrap(); // Received from RPC.
939            assert_eq!(counter, U256::ZERO);
940            let counter_cached = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id).await.unwrap(); // Received from cache.
941            assert_eq!(counter, counter_cached);
942
943            provider.anvil_mine(Some(1), None).await.unwrap();
944
945            // Send a tx incrementing the counter
946            let tx2 = TransactionRequest::default().with_nonce(1).to(counter_addr).input(hex::decode("d09de08a").unwrap().into()).with_chain_id(31337);
947            let receipt2 = provider.send_transaction(tx2).await.unwrap().get_receipt().await.unwrap();
948            let block_id2 = BlockId::number(receipt2.block_number.unwrap());
949
950            let counter2 = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id2).await.unwrap(); // Received from RPC
951            assert_eq!(counter2, U256::from(1));
952            let counter2_cached = provider.get_storage_at(counter_addr, U256::ZERO).block_id(block_id2).await.unwrap(); // Received from cache.
953            assert_eq!(counter2, counter2_cached);
954
955            shared_cache.save_cache(path).unwrap();
956        })
957        .await;
958    }
959
960    #[tokio::test]
961    async fn test_get_transaction_count() {
962        run_with_tempdir("get-tx-count", |dir| async move {
963            let cache_layer = CacheLayer::new(100);
964            // CacheLayer uses Arc internally, so cloning shares the same cache.
965            let cache_layer2 = cache_layer.clone();
966            let shared_cache = cache_layer.cache();
967            let anvil = Anvil::new().spawn();
968            let provider = ProviderBuilder::new()
969                .disable_recommended_fillers()
970                .layer(cache_layer)
971                .connect_http(anvil.endpoint_url());
972
973            let path = dir.join("rpc-cache-tx-count.txt");
974            shared_cache.load_cache(path.clone()).unwrap();
975
976            let address = anvil.addresses()[0];
977
978            // Send a transaction to increase the nonce
979            let req = TransactionRequest::default()
980                .from(address)
981                .to(Address::repeat_byte(5))
982                .value(U256::ZERO)
983                .input(bytes!("deadbeef").into());
984
985            let receipt = provider
986                .send_transaction(req)
987                .await
988                .expect("failed to send tx")
989                .get_receipt()
990                .await
991                .unwrap();
992            let block_number = receipt.block_number.unwrap();
993
994            // Get transaction count from RPC (populates cache)
995            let count = provider
996                .get_transaction_count(address)
997                .block_id(block_number.into())
998                .await
999                .unwrap();
1000            assert_eq!(count, 1);
1001
1002            // Drop anvil to ensure second call can't hit RPC
1003            drop(anvil);
1004
1005            // Create new provider with same cache but dead endpoint
1006            let provider2 = ProviderBuilder::new()
1007                .disable_recommended_fillers()
1008                .layer(cache_layer2)
1009                .connect_http("http://localhost:1".parse().unwrap());
1010
1011            // This only succeeds if cache is hit
1012            let count2 = provider2
1013                .get_transaction_count(address)
1014                .block_id(block_number.into())
1015                .await
1016                .unwrap();
1017            assert_eq!(count, count2);
1018
1019            shared_cache.save_cache(path).unwrap();
1020        })
1021        .await;
1022    }
1023}