Skip to main content

evm_lens_core/
abi.rs

1use std::{collections::HashMap, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
2use tokio::time::sleep;
3
4use async_trait::async_trait;
5use log::error;
6use lru::LruCache;
7use reqwest::{Client, Error as ReqwestError};
8use serde::Deserialize;
9use tokio::{fs, sync::RwLock, task};
10
11/// Maximum number of selectors kept in the in‑memory LRU.
12const DEFAULT_CACHE_SIZE: usize = 5_000;
13
14/// Short alias for a 4‑byte function selector.
15pub type Selector = [u8; 4];
16
17/// Information about one human‑readable function signature.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct SigInfo {
20    /// e.g. "transfer(address,uint256)"
21    pub text: String,
22    pub selector: Selector,
23}
24
25/// Errors that can occur during resolution.
26#[derive(thiserror::Error, Debug)]
27pub enum ResolveError {
28    #[error("network error")]
29    Network(#[from] ReqwestError),
30    #[error("io error")]
31    Io(#[from] std::io::Error),
32    #[error("json error")]
33    Json(#[from] serde_json::Error),
34    #[error("task join error")]
35    Join(#[from] task::JoinError),
36}
37
38/// Async interface for mapping a 4‑byte selector → signatures.
39#[async_trait]
40pub trait SelectorResolver: Send + Sync {
41    async fn resolve(&self, selector: Selector) -> Result<Arc<[SigInfo]>, ResolveError>;
42}
43
44/// “Do‑nothing” implementation, returns an empty slice.
45pub struct NullResolver;
46
47#[async_trait]
48impl SelectorResolver for NullResolver {
49    async fn resolve(&self, _selector: Selector) -> Result<Arc<[SigInfo]>, ResolveError> {
50        Ok(Arc::from([]))
51    }
52}
53
54/// LRU‑cached + on‑disk‑persisted resolver that falls back to 4byte.directory.
55#[derive(Clone)]
56pub struct CompositeResolver {
57    cache: Arc<RwLock<LruCache<Selector, Arc<[SigInfo]>>>>,
58    cache_file: PathBuf,
59    client: Client,
60    base_url: String,
61}
62
63#[derive(Deserialize)]
64struct FourByteEntry {
65    text_signature: String,
66}
67
68#[derive(Deserialize)]
69struct FourByteResponse {
70    #[serde(rename = "results")]
71    results: Vec<FourByteEntry>,
72}
73
74impl CompositeResolver {
75    /// Creates a new resolver. Will eagerly load the on‑disk JSON cache if present.
76    pub async fn new(cache_file: PathBuf, base_url: Option<String>) -> Self {
77        let map: HashMap<String, Vec<String>> = fs::read_to_string(&cache_file)
78            .await
79            .ok()
80            .and_then(|s| serde_json::from_str(&s).ok())
81            .unwrap_or_default();
82
83        let mut lru = LruCache::new(NonZeroUsize::new(DEFAULT_CACHE_SIZE).expect("non‑zero"));
84        for (hex_sel, sigs) in map {
85            if let Ok(bytes) = hex::decode(hex_sel) {
86                if bytes.len() == 4 {
87                    let mut arr = [0u8; 4];
88                    arr.copy_from_slice(&bytes);
89                    let arc = Arc::from(
90                        sigs.into_iter()
91                            .map(|text| SigInfo {
92                                text,
93                                selector: arr,
94                            })
95                            .collect::<Vec<_>>(),
96                    );
97                    lru.put(arr, arc);
98                }
99            }
100        }
101
102        Self {
103            cache: Arc::new(RwLock::new(lru)),
104            cache_file,
105            client: Client::builder()
106                .user_agent("evm-lens (+https://github.com/andyrobert3/evm-lens)")
107                .build()
108                .expect("reqwest client"),
109            base_url: base_url.unwrap_or_else(|| "https://www.4byte.directory".to_string()),
110        }
111    }
112
113    /// Write the current cache map back to disk (atomic rename).
114    async fn persist(&self) -> Result<(), ResolveError> {
115        // Snapshot the cache with a read‑lock then serialize outside the lock.
116        let snapshot: HashMap<String, Vec<String>> = {
117            let cache = self.cache.read().await;
118            cache
119                .iter()
120                .map(|(sel, sigs)| {
121                    let key = hex::encode(sel);
122                    let val = sigs.iter().map(|s| s.text.clone()).collect();
123                    (key, val)
124                })
125                .collect()
126        };
127
128        let tmp = self.cache_file.with_extension("json.tmp");
129        let json = serde_json::to_vec_pretty(&snapshot)?;
130
131        // Use a blocking task for disk I/O.
132        let tmp_clone = tmp.clone();
133        let cache_file = self.cache_file.clone();
134        task::spawn_blocking(move || {
135            std::fs::write(&tmp_clone, &json)?;
136            std::fs::rename(&tmp_clone, &cache_file)?;
137            Ok::<(), std::io::Error>(())
138        })
139        .await??;
140
141        Ok(())
142    }
143
144    /// Query 4byte.directory. Returns an empty vec if the selector is unknown or the
145    /// request fails.
146    async fn fetch_remote(&self, selector: Selector) -> Result<Arc<[SigInfo]>, ResolveError> {
147        let url = format!(
148            "{}/api/v1/signatures/?hex_signature=0x{}",
149            self.base_url,
150            hex::encode(selector)
151        );
152
153        let body = self
154            .client
155            .get(url)
156            .timeout(Duration::from_secs(5))
157            .send()
158            .await?
159            .error_for_status()?
160            .json::<FourByteResponse>()
161            .await?;
162
163        // Add rate limiting delay after successful request
164        sleep(Duration::from_millis(200)).await;
165
166        let sigs: Vec<SigInfo> = body
167            .results
168            .into_iter()
169            .map(|e| SigInfo {
170                text: e.text_signature,
171                selector,
172            })
173            .collect();
174
175        Ok(Arc::from(sigs))
176    }
177}
178
179#[async_trait]
180impl SelectorResolver for CompositeResolver {
181    async fn resolve(&self, selector: Selector) -> Result<Arc<[SigInfo]>, ResolveError> {
182        // Fast path – LRU hit
183        if let Some(hit) = self.cache.read().await.peek(&selector).cloned() {
184            return Ok(hit);
185        }
186
187        // Miss → fetch
188        let sigs = self.fetch_remote(selector).await?;
189
190        if !sigs.is_empty() {
191            self.cache.write().await.put(selector, sigs.clone());
192            // Fire‑and‑forget persistence so we don't block the caller.
193            let slf = self.clone();
194            tokio::spawn(async move {
195                if let Err(e) = slf.persist().await {
196                    error!("persisting cache: {e}");
197                }
198            });
199        }
200
201        Ok(sigs)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use mockito;
209    use serde_json::json;
210    use tempfile::tempdir;
211
212    #[tokio::test]
213    async fn test_null_resolver() {
214        let resolver = NullResolver;
215        let selector = [0xde, 0xad, 0xbe, 0xef];
216        let result = resolver.resolve(selector).await.unwrap();
217        assert!(result.is_empty());
218    }
219
220    #[tokio::test]
221    async fn test_composite_resolver_cache_miss_and_hit() {
222        let selector = [0xca, 0xfe, 0xba, 0xbe];
223        let sig_text = "cafeBabe()";
224        let mock_body = json!({
225            "count": 1,
226            "next": null,
227            "previous": null,
228            "results": [{ "text_signature": sig_text }]
229        });
230
231        let mut server = mockito::Server::new_async().await;
232        let mock_api = server
233            .mock("GET", "/api/v1/signatures/?hex_signature=0xcafebabe")
234            .with_status(200)
235            .with_header("content-type", "application/json")
236            .with_body(mock_body.to_string())
237            .create_async()
238            .await;
239
240        let temp_dir = tempdir().unwrap();
241        let cache_file = temp_dir.path().join("test_cache.json");
242
243        let resolver = CompositeResolver::new(cache_file, Some(server.url())).await;
244
245        // 1. Cache Miss - should hit the mock server
246        let result = resolver.resolve(selector).await.unwrap();
247        assert_eq!(result.len(), 1);
248        assert_eq!(result[0].text, sig_text);
249        assert_eq!(result[0].selector, selector);
250        mock_api.assert_async().await;
251
252        // 2. Cache Hit - should NOT hit the mock server again
253        let result_cached = resolver.resolve(selector).await.unwrap();
254        assert_eq!(result, result_cached);
255        mock_api.assert_async().await; // Assertions are for the total number of hits
256    }
257
258    #[tokio::test]
259    async fn test_composite_resolver_loads_from_disk() {
260        let selector = [0xfe, 0xed, 0xfa, 0xce];
261        let sig_text = "feedFace(uint256)";
262        let cache_content = json!({
263            "feedface": [sig_text]
264        });
265
266        let temp_dir = tempdir().unwrap();
267        let cache_file = temp_dir.path().join("preloaded_cache.json");
268        fs::write(&cache_file, cache_content.to_string())
269            .await
270            .unwrap();
271
272        let mut server = mockito::Server::new_async().await;
273        // The mock should NOT be called if the cache is loaded correctly.
274        let mock_api = server
275            .mock("GET", "/api/v1/signatures/?hex_signature=0xfeedface")
276            .with_status(500) // Expect an error if we miss
277            .expect(0)
278            .create_async()
279            .await;
280
281        let resolver = CompositeResolver::new(cache_file, Some(server.url())).await;
282        let result = resolver.resolve(selector).await.unwrap();
283
284        assert_eq!(result.len(), 1);
285        assert_eq!(result[0].text, sig_text);
286        mock_api.assert_async().await;
287    }
288
289    #[tokio::test]
290    async fn test_composite_resolver_network_error() {
291        let selector = [0xba, 0xad, 0xf0, 0x0d];
292
293        let mut server = mockito::Server::new_async().await;
294        server
295            .mock("GET", "/api/v1/signatures/?hex_signature=0xbaadf00d")
296            .with_status(500) // Simulate a server error
297            .create_async()
298            .await;
299
300        let temp_dir = tempdir().unwrap();
301        let cache_file = temp_dir.path().join("error_cache.json");
302
303        let resolver = CompositeResolver::new(cache_file, Some(server.url())).await;
304
305        let result = resolver.resolve(selector).await;
306        assert!(matches!(result, Err(ResolveError::Network(_))));
307    }
308}