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