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