1use std::{fs, marker::PhantomData, path::PathBuf, time::Duration};
20
21use alloy_chains::Chain;
22use eyre::Result;
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24use tracing::{trace, warn};
25
26pub const DEFAULT_ETHERSCAN_CACHE_TTL: u64 = 86400;
29
30pub trait CachePath {
32 fn edb_cache_dir(&self) -> Option<PathBuf>;
34
35 fn is_valid(&self) -> bool {
37 self.edb_cache_dir().is_some()
38 }
39
40 fn edb_rpc_cache_dir(&self) -> Option<PathBuf> {
42 Some(self.edb_cache_dir()?.join("rpc"))
43 }
44 fn rpc_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
46 Some(self.edb_rpc_cache_dir()?.join(chain_id.into().to_string()))
47 }
48
49 fn etherscan_cache_dir(&self) -> Option<PathBuf> {
51 Some(self.edb_cache_dir()?.join("etherscan"))
52 }
53
54 fn etherscan_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
57 Some(self.etherscan_cache_dir()?.join(chain_id.into().to_string()))
58 }
59
60 fn compiler_cache_dir(&self) -> Option<PathBuf> {
62 Some(self.edb_cache_dir()?.join("solc"))
63 }
64
65 fn compiler_chain_cache_dir(&self, chain_id: impl Into<Chain>) -> Option<PathBuf> {
68 Some(self.compiler_cache_dir()?.join(chain_id.into().to_string()))
69 }
70}
71
72#[derive(Debug, Default)]
74pub struct EdbCachePath {
75 root: Option<PathBuf>,
76}
77
78impl EdbCachePath {
79 pub fn new(root: Option<impl Into<PathBuf>>) -> Self {
81 Self {
82 root: root
83 .map(Into::into)
84 .or_else(|| dirs_next::home_dir().map(|p| p.join(".edb").join("cache"))),
85 }
86 }
87}
88
89impl CachePath for EdbCachePath {
90 fn edb_cache_dir(&self) -> Option<PathBuf> {
91 self.root.clone()
92 }
93}
94
95impl CachePath for Option<EdbCachePath> {
96 fn edb_cache_dir(&self) -> Option<PathBuf> {
97 self.as_ref()?.edb_cache_dir()
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102struct CacheWrapper<T> {
103 pub data: T,
104 pub expires_at: u64,
105}
106
107impl<T> CacheWrapper<T> {
108 pub fn new(data: T, ttl: Option<Duration>) -> Self {
109 Self {
110 data,
111 expires_at: ttl
112 .map(|ttl| ttl.as_secs().saturating_add(chrono::Utc::now().timestamp() as u64))
113 .unwrap_or(u64::MAX),
114 }
115 }
116
117 pub fn is_expired(&self) -> bool {
118 self.expires_at < chrono::Utc::now().timestamp() as u64
119 }
120}
121
122pub trait Cache {
124 type Data: Serialize + DeserializeOwned;
126
127 fn load_cache(&self, label: impl Into<String>) -> Option<Self::Data>;
129
130 fn save_cache(&self, label: impl Into<String>, data: &Self::Data) -> Result<()>;
132}
133
134#[derive(Debug, Clone)]
140pub struct EdbCache<T> {
141 cache_dir: PathBuf,
142 cache_ttl: Option<Duration>,
143 phantom: PhantomData<T>,
144}
145
146impl<T> EdbCache<T>
147where
148 T: Serialize + DeserializeOwned,
149{
150 pub fn new(
152 cache_dir: Option<impl Into<PathBuf>>,
153 cache_ttl: Option<Duration>,
154 ) -> Result<Option<Self>> {
155 if let Some(cache_dir) = cache_dir {
156 let cache_dir = cache_dir.into();
157 fs::create_dir_all(&cache_dir)?;
158 Ok(Some(Self { cache_dir, cache_ttl, phantom: PhantomData }))
159 } else {
160 Ok(None)
161 }
162 }
163
164 pub fn cache_dir(&self) -> &PathBuf {
166 &self.cache_dir
167 }
168
169 pub fn cache_ttl(&self) -> Option<Duration> {
171 self.cache_ttl
172 }
173}
174
175impl<T> Cache for EdbCache<T>
176where
177 T: Serialize + DeserializeOwned,
178{
179 type Data = T;
180
181 fn load_cache(&self, label: impl Into<String>) -> Option<T> {
182 let cache_file = self.cache_dir.join(format!("{}.json", label.into()));
183 trace!("loading cache: {:?}", cache_file);
184 if !cache_file.exists() {
185 return None;
186 }
187
188 let content = fs::read_to_string(&cache_file).ok()?;
189 let cache: CacheWrapper<_> = if let Ok(cache) = serde_json::from_str(&content) {
190 cache
191 } else {
192 warn!("the cache file has been corrupted: {:?}", cache_file);
193 let _ = fs::remove_file(&cache_file); return None;
195 };
196
197 if cache.is_expired() {
198 trace!("the cache file has expired: {:?}", cache_file);
199 let _ = fs::remove_file(&cache_file); None
201 } else {
202 trace!("hit the cache: {:?}", cache_file);
203 Some(cache.data)
204 }
205 }
206
207 fn save_cache(&self, label: impl Into<String>, data: &T) -> Result<()> {
208 let cache_file = self.cache_dir.join(format!("{}.json", label.into()));
209 trace!("saving cache: {:?}", cache_file);
210
211 let cache = CacheWrapper::new(data, self.cache_ttl);
212 let content = serde_json::to_string(&cache)?;
213 fs::write(&cache_file, content)?;
214 Ok(())
215 }
216}
217
218impl<T> Cache for Option<EdbCache<T>>
219where
220 T: Serialize + DeserializeOwned,
221{
222 type Data = T;
223
224 fn load_cache(&self, label: impl Into<String>) -> Option<T> {
225 self.as_ref()?.load_cache(label)
226 }
227
228 fn save_cache(&self, label: impl Into<String>, data: &T) -> Result<()> {
229 if let Some(cache) = self {
230 cache.save_cache(label, data)
231 } else {
232 Ok(())
233 }
234 }
235}