22from collections .abc import Collection
33from concurrent .futures import ThreadPoolExecutor
44from contextlib import contextmanager
5+ from dataclasses import dataclass
56from functools import cached_property
67from pathlib import Path
78from typing import TYPE_CHECKING , Generic , TypeVar
3031_BASE_MODEL = TypeVar ("_BASE_MODEL" , bound = BaseModel )
3132
3233
34+ @dataclass (frozen = True )
35+ class ProxyInfoCacheEntry :
36+ exists : bool
37+ value : ProxyInfoAPI | None = None
38+
39+
3340class ApeDataCache (CacheDirectory , Generic [_BASE_MODEL ]):
3441 """
3542 A wrapper around some cached models in the data directory,
@@ -47,7 +54,7 @@ def __init__(
4754 data_folder = base_data_folder / ecosystem_key
4855 base_path = data_folder / network_key
4956 self ._model_type = model_type
50- self .memory : dict [str , _BASE_MODEL ] = {}
57+ self .memory : dict [str , _BASE_MODEL | None ] = {}
5158
5259 # Only write if we are not testing!
5360 self ._write_to_disk = not network_key .endswith ("-fork" ) and network_key != "local"
@@ -92,6 +99,60 @@ def get_type(self, key: str, fetch_from_disk: bool = True) -> _BASE_MODEL | None
9299 return None
93100
94101
102+ class ProxyInfoCache (ApeDataCache [ProxyInfoAPI ]):
103+ """
104+ Cache of proxy detection results.
105+
106+ A missing file means unchecked, `null` means checked and not a proxy, and a
107+ JSON object means checked and proxy info found.
108+ """
109+
110+ def __init__ (
111+ self ,
112+ base_data_folder : Path ,
113+ ecosystem_key : str ,
114+ network_key : str ,
115+ key : str ,
116+ model_type : type [ProxyInfoAPI ],
117+ ):
118+ super ().__init__ (base_data_folder , ecosystem_key , network_key , key , model_type )
119+ self .memory : dict [str , ProxyInfoAPI | None ] = {}
120+
121+ def __setitem__ (self , key : str , value : ProxyInfoAPI | None ): # type: ignore
122+ self .memory [key ] = value
123+ if self ._write_to_disk :
124+ self .cache_data (key , value .model_dump (mode = "json" ) if value is not None else None )
125+
126+ def __delitem__ (self , key : str ):
127+ super ().__delitem__ (key )
128+
129+ def get_type (self , key : str , fetch_from_disk : bool = True ) -> ProxyInfoAPI | None :
130+ return self .get_entry (key , fetch_from_disk = fetch_from_disk ).value
131+
132+ def get_entry (self , key : str , fetch_from_disk : bool = True ) -> ProxyInfoCacheEntry :
133+ if key in self .memory :
134+ return ProxyInfoCacheEntry (exists = True , value = self .memory [key ])
135+
136+ elif fetch_from_disk and self ._read_from_disk :
137+ file = self .get_file (key )
138+ if file .is_file ():
139+ data = self .get_data (key )
140+ if data is None :
141+ self .memory [key ] = None
142+ return ProxyInfoCacheEntry (exists = True )
143+
144+ # Found proxy info on disk.
145+ model = self ._model_type .model_validate (data )
146+ # Cache locally for next time.
147+ self .memory [key ] = model
148+ return ProxyInfoCacheEntry (exists = True , value = model )
149+
150+ return ProxyInfoCacheEntry (exists = False )
151+
152+ def clear_memory (self ):
153+ self .memory = {}
154+
155+
95156class ContractCache (BaseManager ):
96157 """
97158 A collection of cached contracts. Contracts can be cached in two ways:
@@ -115,8 +176,8 @@ def contract_types(self) -> ApeDataCache[ContractType]:
115176 return self ._get_data_cache ("contract_types" , ContractType )
116177
117178 @property
118- def proxy_infos (self ) -> ApeDataCache [ ProxyInfoAPI ] :
119- return self ._get_data_cache ("proxy_info" , ProxyInfoAPI )
179+ def proxy_infos (self ) -> ProxyInfoCache :
180+ return self ._get_data_cache ("proxy_info" , ProxyInfoAPI , cache_type = ProxyInfoCache )
120181
121182 @property
122183 def blueprints (self ) -> ApeDataCache [ContractType ]:
@@ -132,6 +193,7 @@ def _get_data_cache(
132193 model_type : type ,
133194 ecosystem_key : str | None = None ,
134195 network_key : str | None = None ,
196+ cache_type : type [ApeDataCache ] = ApeDataCache ,
135197 ):
136198 ecosystem_name = ecosystem_key or self .provider .network .ecosystem .name
137199 network_name = network_key or self .provider .network .name .replace ("-fork" , "" )
@@ -141,7 +203,7 @@ def _get_data_cache(
141203 if cache := self ._caches [ecosystem_name ][network_name ].get (key ):
142204 return cache
143205
144- self ._caches [ecosystem_name ][network_name ][key ] = ApeDataCache (
206+ self ._caches [ecosystem_name ][network_name ][key ] = cache_type (
145207 self .config_manager .DATA_FOLDER , ecosystem_name , network_name , key , model_type
146208 )
147209 return self ._caches [ecosystem_name ][network_name ][key ]
@@ -178,6 +240,15 @@ def __setitem__(
178240 else :
179241 raise TypeError (item )
180242
243+ def cache_proxy_info_no_hit (self , address : AddressType ):
244+ """
245+ Cache that proxy detection found no proxy information for this address.
246+
247+ Args:
248+ address (AddressType): The address that is not a proxy.
249+ """
250+ self .proxy_infos [address ] = None
251+
181252 def cache_contract_type (
182253 self ,
183254 address : AddressType ,
@@ -267,6 +338,8 @@ def _delete_proxy(self, address: AddressType):
267338 target = info .target
268339 del self .proxy_infos [target ]
269340 del self .contract_types [target ]
341+ else :
342+ del self .proxy_infos [address ]
270343
271344 def __contains__ (self , address : AddressType ) -> bool :
272345 return self .get (address ) is not None
@@ -307,6 +380,7 @@ def cache_deployment(
307380
308381 else :
309382 # Cache as normal.
383+ self .cache_proxy_info_no_hit (address )
310384 self .contract_types [address ] = contract_type
311385
312386 else :
@@ -584,9 +658,16 @@ def get(
584658 # Check broader sources, such as an explorer.
585659 if not proxy_info and detect_proxy :
586660 # Proxy info not provided. Attempt to detect.
587- if not (proxy_info := self .proxy_infos [address_key ]):
661+ cached_proxy_info = self .proxy_infos .get_entry (
662+ address_key , fetch_from_disk = fetch_from_disk
663+ )
664+ if cached_proxy_info .exists :
665+ proxy_info = cached_proxy_info .value
666+ else :
588667 if proxy_info := self .provider .network .ecosystem .get_proxy_info (address_key ):
589- self .proxy_infos [address_key ] = proxy_info
668+ self .cache_proxy_info (address_key , proxy_info )
669+ else :
670+ self .cache_proxy_info_no_hit (address_key )
590671
591672 if proxy_info :
592673 if proxy_contract_type := self ._get_proxy_contract_type (
@@ -917,7 +998,10 @@ def clear_local_caches(self):
917998 self .contract_creations ,
918999 self .blueprints ,
9191000 ):
920- cache .memory = {}
1001+ if isinstance (cache , ProxyInfoCache ):
1002+ cache .clear_memory ()
1003+ else :
1004+ cache .memory = {}
9211005
9221006 self .deployments .clear_local ()
9231007
0 commit comments