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