alloy_provider/layers/
cache.rs

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