async-substrate-interface 1.2.2__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/PKG-INFO +1 -1
  2. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/async_substrate.py +51 -33
  3. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/errors.py +10 -0
  4. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/substrate_addons.py +40 -9
  5. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/sync_substrate.py +13 -6
  6. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/utils/cache.py +56 -0
  7. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface.egg-info/PKG-INFO +1 -1
  8. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/pyproject.toml +1 -1
  9. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/tests/test_substrate_addons.py +31 -2
  10. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/LICENSE +0 -0
  11. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/README.md +0 -0
  12. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/__init__.py +0 -0
  13. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/const.py +0 -0
  14. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/protocols.py +0 -0
  15. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/type_registry.py +0 -0
  16. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/types.py +0 -0
  17. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/utils/__init__.py +0 -0
  18. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/utils/decoding.py +0 -0
  19. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/utils/hasher.py +0 -0
  20. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface/utils/storage.py +0 -0
  21. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface.egg-info/SOURCES.txt +0 -0
  22. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface.egg-info/dependency_links.txt +0 -0
  23. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface.egg-info/requires.txt +0 -0
  24. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/async_substrate_interface.egg-info/top_level.txt +0 -0
  25. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/setup.cfg +0 -0
  26. {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.0}/tests/test_old_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-substrate-interface
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface
5
5
  Author: Opentensor Foundation
6
6
  Author-email: BD Himes <b@latent.to>
@@ -22,7 +22,6 @@ from typing import (
22
22
  TYPE_CHECKING,
23
23
  )
24
24
 
25
- import asyncstdlib as a
26
25
  from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
27
26
  from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject
28
27
  from scalecodec.types import (
@@ -42,6 +41,7 @@ from async_substrate_interface.errors import (
42
41
  BlockNotFound,
43
42
  MaxRetriesExceeded,
44
43
  MetadataAtVersionNotFound,
44
+ StateDiscardedError,
45
45
  )
46
46
  from async_substrate_interface.protocols import Keypair
47
47
  from async_substrate_interface.types import (
@@ -58,7 +58,7 @@ from async_substrate_interface.utils import (
58
58
  get_next_id,
59
59
  rng as random,
60
60
  )
61
- from async_substrate_interface.utils.cache import async_sql_lru_cache
61
+ from async_substrate_interface.utils.cache import async_sql_lru_cache, CachedFetcher
62
62
  from async_substrate_interface.utils.decoding import (
63
63
  _determine_if_old_runtime_call,
64
64
  _bt_decode_to_dict_or_list,
@@ -539,14 +539,17 @@ class Websocket:
539
539
  "You are instantiating the AsyncSubstrateInterface Websocket outside of an event loop. "
540
540
  "Verify this is intended."
541
541
  )
542
- now = asyncio.new_event_loop().time()
542
+ # default value for in case there's no running asyncio loop
543
+ # this really doesn't matter in most cases, as it's only used for comparison on the first call to
544
+ # see how long it's been since the last call
545
+ now = 0.0
543
546
  self.last_received = now
544
547
  self.last_sent = now
548
+ self._in_use_ids = set()
545
549
 
546
550
  async def __aenter__(self):
547
- async with self._lock:
548
- self._in_use += 1
549
- await self.connect()
551
+ self._in_use += 1
552
+ await self.connect()
550
553
  return self
551
554
 
552
555
  @staticmethod
@@ -559,18 +562,19 @@ class Websocket:
559
562
  self.last_sent = now
560
563
  if self._exit_task:
561
564
  self._exit_task.cancel()
562
- if not self._initialized or force:
563
- self._initialized = True
564
- try:
565
- self._receiving_task.cancel()
566
- await self._receiving_task
567
- await self.ws.close()
568
- except (AttributeError, asyncio.CancelledError):
569
- pass
570
- self.ws = await asyncio.wait_for(
571
- connect(self.ws_url, **self._options), timeout=10
572
- )
573
- self._receiving_task = asyncio.create_task(self._start_receiving())
565
+ async with self._lock:
566
+ if not self._initialized or force:
567
+ try:
568
+ self._receiving_task.cancel()
569
+ await self._receiving_task
570
+ await self.ws.close()
571
+ except (AttributeError, asyncio.CancelledError):
572
+ pass
573
+ self.ws = await asyncio.wait_for(
574
+ connect(self.ws_url, **self._options), timeout=10
575
+ )
576
+ self._receiving_task = asyncio.create_task(self._start_receiving())
577
+ self._initialized = True
574
578
 
575
579
  async def __aexit__(self, exc_type, exc_val, exc_tb):
576
580
  async with self._lock: # TODO is this actually what I want to happen?
@@ -619,6 +623,7 @@ class Websocket:
619
623
  self._open_subscriptions -= 1
620
624
  if "id" in response:
621
625
  self._received[response["id"]] = response
626
+ self._in_use_ids.remove(response["id"])
622
627
  elif "params" in response:
623
628
  self._received[response["params"]["subscription"]] = response
624
629
  else:
@@ -649,6 +654,9 @@ class Websocket:
649
654
  id: the internal ID of the request (incremented int)
650
655
  """
651
656
  original_id = get_next_id()
657
+ while original_id in self._in_use_ids:
658
+ original_id = get_next_id()
659
+ self._in_use_ids.add(original_id)
652
660
  # self._open_subscriptions += 1
653
661
  await self.max_subscriptions.acquire()
654
662
  try:
@@ -674,7 +682,7 @@ class Websocket:
674
682
  self.max_subscriptions.release()
675
683
  return item
676
684
  except KeyError:
677
- await asyncio.sleep(0.001)
685
+ await asyncio.sleep(0.1)
678
686
  return None
679
687
 
680
688
 
@@ -725,6 +733,7 @@ class AsyncSubstrateInterface(SubstrateMixin):
725
733
  )
726
734
  else:
727
735
  self.ws = AsyncMock(spec=Websocket)
736
+
728
737
  self._lock = asyncio.Lock()
729
738
  self.config = {
730
739
  "use_remote_preset": use_remote_preset,
@@ -748,6 +757,12 @@ class AsyncSubstrateInterface(SubstrateMixin):
748
757
  self.registry_type_map = {}
749
758
  self.type_id_to_name = {}
750
759
  self._mock = _mock
760
+ self._block_hash_fetcher = CachedFetcher(512, self._get_block_hash)
761
+ self._parent_hash_fetcher = CachedFetcher(512, self._get_parent_block_hash)
762
+ self._runtime_info_fetcher = CachedFetcher(16, self._get_block_runtime_info)
763
+ self._runtime_version_for_fetcher = CachedFetcher(
764
+ 512, self._get_block_runtime_version_for
765
+ )
751
766
 
752
767
  async def __aenter__(self):
753
768
  if not self._mock:
@@ -1869,9 +1884,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
1869
1884
 
1870
1885
  return runtime.metadata_v15
1871
1886
 
1872
- @a.lru_cache(maxsize=512)
1873
1887
  async def get_parent_block_hash(self, block_hash):
1874
- return await self._get_parent_block_hash(block_hash)
1888
+ return await self._parent_hash_fetcher.execute(block_hash)
1875
1889
 
1876
1890
  async def _get_parent_block_hash(self, block_hash):
1877
1891
  block_header = await self.rpc_request("chain_getHeader", [block_hash])
@@ -1916,9 +1930,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
1916
1930
  "Unknown error occurred during retrieval of events"
1917
1931
  )
1918
1932
 
1919
- @a.lru_cache(maxsize=16)
1920
1933
  async def get_block_runtime_info(self, block_hash: str) -> dict:
1921
- return await self._get_block_runtime_info(block_hash)
1934
+ return await self._runtime_info_fetcher.execute(block_hash)
1922
1935
 
1923
1936
  get_block_runtime_version = get_block_runtime_info
1924
1937
 
@@ -1929,9 +1942,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
1929
1942
  response = await self.rpc_request("state_getRuntimeVersion", [block_hash])
1930
1943
  return response.get("result")
1931
1944
 
1932
- @a.lru_cache(maxsize=512)
1933
1945
  async def get_block_runtime_version_for(self, block_hash: str):
1934
- return await self._get_block_runtime_version_for(block_hash)
1946
+ return await self._runtime_version_for_fetcher.execute(block_hash)
1935
1947
 
1936
1948
  async def _get_block_runtime_version_for(self, block_hash: str):
1937
1949
  """
@@ -2137,6 +2149,7 @@ class AsyncSubstrateInterface(SubstrateMixin):
2137
2149
  storage_item,
2138
2150
  result_handler,
2139
2151
  )
2152
+
2140
2153
  request_manager.add_response(
2141
2154
  item_id, decoded_response, complete
2142
2155
  )
@@ -2149,14 +2162,14 @@ class AsyncSubstrateInterface(SubstrateMixin):
2149
2162
  and current_time - self.ws.last_sent >= self.retry_timeout
2150
2163
  ):
2151
2164
  if attempt >= self.max_retries:
2152
- logger.warning(
2165
+ logger.error(
2153
2166
  f"Timed out waiting for RPC requests {attempt} times. Exiting."
2154
2167
  )
2155
2168
  raise MaxRetriesExceeded("Max retries reached.")
2156
2169
  else:
2157
2170
  self.ws.last_received = time.time()
2158
2171
  await self.ws.connect(force=True)
2159
- logger.error(
2172
+ logger.warning(
2160
2173
  f"Timed out waiting for RPC requests. "
2161
2174
  f"Retrying attempt {attempt + 1} of {self.max_retries}"
2162
2175
  )
@@ -2223,9 +2236,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
2223
2236
  ]
2224
2237
  result = await self._make_rpc_request(payloads, result_handler=result_handler)
2225
2238
  if "error" in result[payload_id][0]:
2226
- if (
2227
- "Failed to get runtime version"
2228
- in result[payload_id][0]["error"]["message"]
2239
+ if "Failed to get runtime version" in (
2240
+ err_msg := result[payload_id][0]["error"]["message"]
2229
2241
  ):
2230
2242
  logger.warning(
2231
2243
  "Failed to get runtime. Re-fetching from chain, and retrying."
@@ -2234,15 +2246,21 @@ class AsyncSubstrateInterface(SubstrateMixin):
2234
2246
  return await self.rpc_request(
2235
2247
  method, params, result_handler, block_hash, reuse_block_hash
2236
2248
  )
2237
- raise SubstrateRequestException(result[payload_id][0]["error"]["message"])
2249
+ elif (
2250
+ "Client error: Api called for an unknown Block: State already discarded"
2251
+ in err_msg
2252
+ ):
2253
+ bh = err_msg.split("State already discarded for ")[1].strip()
2254
+ raise StateDiscardedError(bh)
2255
+ else:
2256
+ raise SubstrateRequestException(err_msg)
2238
2257
  if "result" in result[payload_id][0]:
2239
2258
  return result[payload_id][0]
2240
2259
  else:
2241
2260
  raise SubstrateRequestException(result[payload_id][0])
2242
2261
 
2243
- @a.lru_cache(maxsize=512)
2244
2262
  async def get_block_hash(self, block_id: int) -> str:
2245
- return await self._get_block_hash(block_id)
2263
+ return await self._block_hash_fetcher.execute(block_id)
2246
2264
 
2247
2265
  async def _get_block_hash(self, block_id: int) -> str:
2248
2266
  return (await self.rpc_request("chain_getBlockHash", [block_id]))["result"]
@@ -22,6 +22,16 @@ class MetadataAtVersionNotFound(SubstrateRequestException):
22
22
  super().__init__(message)
23
23
 
24
24
 
25
+ class StateDiscardedError(SubstrateRequestException):
26
+ def __init__(self, block_hash: str):
27
+ self.block_hash = block_hash
28
+ message = (
29
+ f"State discarded for {block_hash}. This indicates the block is too old, and you should instead "
30
+ f"make this request using an archive node."
31
+ )
32
+ super().__init__(message)
33
+
34
+
25
35
  class StorageFunctionNotFound(ValueError):
26
36
  pass
27
37
 
@@ -13,7 +13,7 @@ from typing import Optional
13
13
  from websockets.exceptions import ConnectionClosed
14
14
 
15
15
  from async_substrate_interface.async_substrate import AsyncSubstrateInterface, Websocket
16
- from async_substrate_interface.errors import MaxRetriesExceeded
16
+ from async_substrate_interface.errors import MaxRetriesExceeded, StateDiscardedError
17
17
  from async_substrate_interface.sync_substrate import SubstrateInterface
18
18
 
19
19
  logger = logging.getLogger("async_substrate_interface")
@@ -117,6 +117,7 @@ class RetrySyncSubstrate(SubstrateInterface):
117
117
  max_retries: int = 5,
118
118
  retry_timeout: float = 60.0,
119
119
  _mock: bool = False,
120
+ archive_nodes: Optional[list[str]] = None,
120
121
  ):
121
122
  fallback_chains = fallback_chains or []
122
123
  self.fallback_chains = (
@@ -124,6 +125,9 @@ class RetrySyncSubstrate(SubstrateInterface):
124
125
  if not retry_forever
125
126
  else cycle(fallback_chains + [url])
126
127
  )
128
+ self.archive_nodes = (
129
+ iter(archive_nodes) if not retry_forever else cycle(archive_nodes)
130
+ )
127
131
  self.use_remote_preset = use_remote_preset
128
132
  self.chain_name = chain_name
129
133
  self._mock = _mock
@@ -174,9 +178,12 @@ class RetrySyncSubstrate(SubstrateInterface):
174
178
  EOFError,
175
179
  ConnectionClosed,
176
180
  TimeoutError,
181
+ socket.gaierror,
182
+ StateDiscardedError,
177
183
  ) as e:
184
+ use_archive = isinstance(e, StateDiscardedError)
178
185
  try:
179
- self._reinstantiate_substrate(e)
186
+ self._reinstantiate_substrate(e, use_archive=use_archive)
180
187
  return method_(*args, **kwargs)
181
188
  except StopIteration:
182
189
  logger.error(
@@ -184,10 +191,19 @@ class RetrySyncSubstrate(SubstrateInterface):
184
191
  )
185
192
  raise MaxRetriesExceeded
186
193
 
187
- def _reinstantiate_substrate(self, e: Optional[Exception] = None) -> None:
188
- next_network = next(self.fallback_chains)
194
+ def _reinstantiate_substrate(
195
+ self, e: Optional[Exception] = None, use_archive: bool = False
196
+ ) -> None:
197
+ if use_archive:
198
+ bh = getattr(e, "block_hash", "Unknown Block Hash")
199
+ logger.info(
200
+ f"Attempt made to {bh} failed for state discarded. Attempting to switch to archive node."
201
+ )
202
+ next_network = next(self.archive_nodes)
203
+ else:
204
+ next_network = next(self.fallback_chains)
189
205
  self.ws.close()
190
- if e.__class__ == MaxRetriesExceeded:
206
+ if isinstance(e, MaxRetriesExceeded):
191
207
  logger.error(
192
208
  f"Max retries exceeded with {self.url}. Retrying with {next_network}."
193
209
  )
@@ -243,6 +259,7 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
243
259
  max_retries: int = 5,
244
260
  retry_timeout: float = 60.0,
245
261
  _mock: bool = False,
262
+ archive_nodes: Optional[list[str]] = None,
246
263
  ):
247
264
  fallback_chains = fallback_chains or []
248
265
  self.fallback_chains = (
@@ -250,6 +267,9 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
250
267
  if not retry_forever
251
268
  else cycle(fallback_chains + [url])
252
269
  )
270
+ self.archive_nodes = (
271
+ iter(archive_nodes) if not retry_forever else cycle(archive_nodes)
272
+ )
253
273
  self.use_remote_preset = use_remote_preset
254
274
  self.chain_name = chain_name
255
275
  self._mock = _mock
@@ -272,9 +292,18 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
272
292
  for method in RETRY_METHODS:
273
293
  setattr(self, method, partial(self._retry, method))
274
294
 
275
- async def _reinstantiate_substrate(self, e: Optional[Exception] = None) -> None:
276
- next_network = next(self.fallback_chains)
277
- if e.__class__ == MaxRetriesExceeded:
295
+ async def _reinstantiate_substrate(
296
+ self, e: Optional[Exception] = None, use_archive: bool = False
297
+ ) -> None:
298
+ if use_archive:
299
+ bh = getattr(e, "block_hash", "Unknown Block Hash")
300
+ logger.info(
301
+ f"Attempt made to {bh} failed for state discarded. Attempting to switch to archive node."
302
+ )
303
+ next_network = next(self.archive_nodes)
304
+ else:
305
+ next_network = next(self.fallback_chains)
306
+ if isinstance(e, MaxRetriesExceeded):
278
307
  logger.error(
279
308
  f"Max retries exceeded with {self.url}. Retrying with {next_network}."
280
309
  )
@@ -314,9 +343,11 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
314
343
  ConnectionClosed,
315
344
  EOFError,
316
345
  socket.gaierror,
346
+ StateDiscardedError,
317
347
  ) as e:
348
+ use_archive = isinstance(e, StateDiscardedError)
318
349
  try:
319
- await self._reinstantiate_substrate(e)
350
+ await self._reinstantiate_substrate(e, use_archive=use_archive)
320
351
  return await method_(*args, **kwargs)
321
352
  except StopAsyncIteration:
322
353
  logger.error(
@@ -24,6 +24,7 @@ from async_substrate_interface.errors import (
24
24
  BlockNotFound,
25
25
  MaxRetriesExceeded,
26
26
  MetadataAtVersionNotFound,
27
+ StateDiscardedError,
27
28
  )
28
29
  from async_substrate_interface.protocols import Keypair
29
30
  from async_substrate_interface.types import (
@@ -1944,9 +1945,8 @@ class SubstrateInterface(SubstrateMixin):
1944
1945
  ]
1945
1946
  result = self._make_rpc_request(payloads, result_handler=result_handler)
1946
1947
  if "error" in result[payload_id][0]:
1947
- if (
1948
- "Failed to get runtime version"
1949
- in result[payload_id][0]["error"]["message"]
1948
+ if "Failed to get runtime version" in (
1949
+ err_msg := result[payload_id][0]["error"]["message"]
1950
1950
  ):
1951
1951
  logger.warning(
1952
1952
  "Failed to get runtime. Re-fetching from chain, and retrying."
@@ -1955,7 +1955,14 @@ class SubstrateInterface(SubstrateMixin):
1955
1955
  return self.rpc_request(
1956
1956
  method, params, result_handler, block_hash, reuse_block_hash
1957
1957
  )
1958
- raise SubstrateRequestException(result[payload_id][0]["error"]["message"])
1958
+ elif (
1959
+ "Client error: Api called for an unknown Block: State already discarded"
1960
+ in err_msg
1961
+ ):
1962
+ bh = err_msg.split("State already discarded for ")[1].strip()
1963
+ raise StateDiscardedError(bh)
1964
+ else:
1965
+ raise SubstrateRequestException(err_msg)
1959
1966
  if "result" in result[payload_id][0]:
1960
1967
  return result[payload_id][0]
1961
1968
  else:
@@ -2497,13 +2504,13 @@ class SubstrateInterface(SubstrateMixin):
2497
2504
  Returns:
2498
2505
  ScaleType from the runtime call
2499
2506
  """
2500
- self.init_runtime(block_hash=block_hash)
2507
+ runtime = self.init_runtime(block_hash=block_hash)
2501
2508
 
2502
2509
  if params is None:
2503
2510
  params = {}
2504
2511
 
2505
2512
  try:
2506
- metadata_v15_value = self.runtime.metadata_v15.value()
2513
+ metadata_v15_value = runtime.metadata_v15.value()
2507
2514
 
2508
2515
  apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
2509
2516
  api_entry = apis[api]
@@ -1,10 +1,15 @@
1
+ import asyncio
2
+ from collections import OrderedDict
1
3
  import functools
2
4
  import os
3
5
  import pickle
4
6
  import sqlite3
5
7
  from pathlib import Path
8
+ from typing import Callable, Any
9
+
6
10
  import asyncstdlib as a
7
11
 
12
+
8
13
  USE_CACHE = True if os.getenv("NO_CACHE") != "1" else False
9
14
  CACHE_LOCATION = (
10
15
  os.path.expanduser(
@@ -139,3 +144,54 @@ def async_sql_lru_cache(maxsize=None):
139
144
  return inner
140
145
 
141
146
  return decorator
147
+
148
+
149
+ class LRUCache:
150
+ def __init__(self, max_size: int):
151
+ self.max_size = max_size
152
+ self.cache = OrderedDict()
153
+
154
+ def set(self, key, value):
155
+ if key in self.cache:
156
+ self.cache.move_to_end(key)
157
+ self.cache[key] = value
158
+ if len(self.cache) > self.max_size:
159
+ self.cache.popitem(last=False)
160
+
161
+ def get(self, key):
162
+ if key in self.cache:
163
+ # Mark as recently used
164
+ self.cache.move_to_end(key)
165
+ return self.cache[key]
166
+ return None
167
+
168
+
169
+ class CachedFetcher:
170
+ def __init__(self, max_size: int, method: Callable):
171
+ self._inflight: dict[int, asyncio.Future] = {}
172
+ self._method = method
173
+ self._cache = LRUCache(max_size=max_size)
174
+
175
+ async def execute(self, single_arg: Any) -> str:
176
+ if item := self._cache.get(single_arg):
177
+ return item
178
+
179
+ if single_arg in self._inflight:
180
+ result = await self._inflight[single_arg]
181
+ return result
182
+
183
+ loop = asyncio.get_running_loop()
184
+ future = loop.create_future()
185
+ self._inflight[single_arg] = future
186
+
187
+ try:
188
+ result = await self._method(single_arg)
189
+ self._cache.set(single_arg, result)
190
+ future.set_result(result)
191
+ return result
192
+ except Exception as e:
193
+ # Propagate errors
194
+ future.set_exception(e)
195
+ raise
196
+ finally:
197
+ self._inflight.pop(single_arg, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-substrate-interface
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface
5
5
  Author: Opentensor Foundation
6
6
  Author-email: BD Himes <b@latent.to>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-substrate-interface"
3
- version = "1.2.2"
3
+ version = "1.3.0"
4
4
  description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -4,8 +4,12 @@ import subprocess
4
4
  import pytest
5
5
  import time
6
6
 
7
- from async_substrate_interface.substrate_addons import RetrySyncSubstrate
8
- from async_substrate_interface.errors import MaxRetriesExceeded
7
+ from async_substrate_interface import AsyncSubstrateInterface, SubstrateInterface
8
+ from async_substrate_interface.substrate_addons import (
9
+ RetrySyncSubstrate,
10
+ RetryAsyncSubstrate,
11
+ )
12
+ from async_substrate_interface.errors import MaxRetriesExceeded, StateDiscardedError
9
13
  from tests.conftest import start_docker_container
10
14
 
11
15
  LATENT_LITE_ENTRYPOINT = "wss://lite.sub.latent.to:443"
@@ -70,3 +74,28 @@ def test_retry_sync_substrate_offline():
70
74
  RetrySyncSubstrate(
71
75
  "ws://127.0.0.1:9945", fallback_chains=["ws://127.0.0.1:9946"]
72
76
  )
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_retry_async_subtensor_archive_node():
81
+ async with AsyncSubstrateInterface("wss://lite.sub.latent.to:443") as substrate:
82
+ current_block = await substrate.get_block_number()
83
+ old_block = current_block - 1000
84
+ with pytest.raises(StateDiscardedError):
85
+ await substrate.get_block(block_number=old_block)
86
+ async with RetryAsyncSubstrate(
87
+ "wss://lite.sub.latent.to:443", archive_nodes=["ws://178.156.172.75:9944"]
88
+ ) as substrate:
89
+ assert isinstance((await substrate.get_block(block_number=old_block)), dict)
90
+
91
+
92
+ def test_retry_sync_subtensor_archive_node():
93
+ with SubstrateInterface("wss://lite.sub.latent.to:443") as substrate:
94
+ current_block = substrate.get_block_number()
95
+ old_block = current_block - 1000
96
+ with pytest.raises(StateDiscardedError):
97
+ substrate.get_block(block_number=old_block)
98
+ with RetrySyncSubstrate(
99
+ "wss://lite.sub.latent.to:443", archive_nodes=["ws://178.156.172.75:9944"]
100
+ ) as substrate:
101
+ assert isinstance((substrate.get_block(block_number=old_block)), dict)