async-substrate-interface 1.2.2__tar.gz → 1.3.1__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.
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/PKG-INFO +1 -1
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/async_substrate.py +125 -70
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/errors.py +10 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/substrate_addons.py +49 -10
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/sync_substrate.py +25 -8
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/utils/cache.py +56 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface.egg-info/PKG-INFO +1 -1
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/pyproject.toml +1 -1
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/tests/test_substrate_addons.py +31 -2
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/LICENSE +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/README.md +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/__init__.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/const.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/protocols.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/type_registry.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/types.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/utils/__init__.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/utils/decoding.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/utils/hasher.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface/utils/storage.py +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface.egg-info/SOURCES.txt +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface.egg-info/dependency_links.txt +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface.egg-info/requires.txt +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/async_substrate_interface.egg-info/top_level.txt +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/setup.cfg +0 -0
- {async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/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.
|
|
3
|
+
Version: 1.3.1
|
|
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 (
|
|
@@ -33,7 +32,7 @@ from scalecodec.types import (
|
|
|
33
32
|
MultiAccountId,
|
|
34
33
|
)
|
|
35
34
|
from websockets.asyncio.client import connect
|
|
36
|
-
from websockets.exceptions import ConnectionClosed
|
|
35
|
+
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
37
36
|
|
|
38
37
|
from async_substrate_interface.const import SS58_FORMAT
|
|
39
38
|
from async_substrate_interface.errors 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,
|
|
@@ -75,6 +75,7 @@ if TYPE_CHECKING:
|
|
|
75
75
|
ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]]
|
|
76
76
|
|
|
77
77
|
logger = logging.getLogger("async_substrate_interface")
|
|
78
|
+
raw_websocket_logger = logging.getLogger("raw_websocket")
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
class AsyncExtrinsicReceipt:
|
|
@@ -505,6 +506,7 @@ class Websocket:
|
|
|
505
506
|
max_connections=100,
|
|
506
507
|
shutdown_timer=5,
|
|
507
508
|
options: Optional[dict] = None,
|
|
509
|
+
_log_raw_websockets: bool = False,
|
|
508
510
|
):
|
|
509
511
|
"""
|
|
510
512
|
Websocket manager object. Allows for the use of a single websocket connection by multiple
|
|
@@ -532,6 +534,10 @@ class Websocket:
|
|
|
532
534
|
self._exit_task = None
|
|
533
535
|
self._open_subscriptions = 0
|
|
534
536
|
self._options = options if options else {}
|
|
537
|
+
self._log_raw_websockets = _log_raw_websockets
|
|
538
|
+
self._is_connecting = False
|
|
539
|
+
self._is_closing = False
|
|
540
|
+
|
|
535
541
|
try:
|
|
536
542
|
now = asyncio.get_running_loop().time()
|
|
537
543
|
except RuntimeError:
|
|
@@ -539,51 +545,80 @@ class Websocket:
|
|
|
539
545
|
"You are instantiating the AsyncSubstrateInterface Websocket outside of an event loop. "
|
|
540
546
|
"Verify this is intended."
|
|
541
547
|
)
|
|
542
|
-
|
|
548
|
+
# default value for in case there's no running asyncio loop
|
|
549
|
+
# this really doesn't matter in most cases, as it's only used for comparison on the first call to
|
|
550
|
+
# see how long it's been since the last call
|
|
551
|
+
now = 0.0
|
|
543
552
|
self.last_received = now
|
|
544
553
|
self.last_sent = now
|
|
554
|
+
self._in_use_ids = set()
|
|
545
555
|
|
|
546
556
|
async def __aenter__(self):
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
await self.connect()
|
|
557
|
+
self._in_use += 1
|
|
558
|
+
await self.connect()
|
|
550
559
|
return self
|
|
551
560
|
|
|
552
561
|
@staticmethod
|
|
553
562
|
async def loop_time() -> float:
|
|
554
563
|
return asyncio.get_running_loop().time()
|
|
555
564
|
|
|
556
|
-
async def
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
self.ws = await asyncio.wait_for(
|
|
571
|
-
connect(self.ws_url, **self._options), timeout=10
|
|
565
|
+
async def _cancel(self):
|
|
566
|
+
try:
|
|
567
|
+
self._receiving_task.cancel()
|
|
568
|
+
await self._receiving_task
|
|
569
|
+
await self.ws.close()
|
|
570
|
+
except (
|
|
571
|
+
AttributeError,
|
|
572
|
+
asyncio.CancelledError,
|
|
573
|
+
WebSocketException,
|
|
574
|
+
):
|
|
575
|
+
pass
|
|
576
|
+
except Exception as e:
|
|
577
|
+
logger.warning(
|
|
578
|
+
f"{e} encountered while trying to close websocket connection."
|
|
572
579
|
)
|
|
573
|
-
self._receiving_task = asyncio.create_task(self._start_receiving())
|
|
574
580
|
|
|
575
|
-
async def
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
581
|
+
async def connect(self, force=False):
|
|
582
|
+
self._is_connecting = True
|
|
583
|
+
try:
|
|
584
|
+
now = await self.loop_time()
|
|
585
|
+
self.last_received = now
|
|
586
|
+
self.last_sent = now
|
|
587
|
+
if self._exit_task:
|
|
579
588
|
self._exit_task.cancel()
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
589
|
+
if not self._is_closing:
|
|
590
|
+
if not self._initialized or force:
|
|
591
|
+
try:
|
|
592
|
+
await asyncio.wait_for(self._cancel(), timeout=10.0)
|
|
593
|
+
except asyncio.TimeoutError:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
self.ws = await asyncio.wait_for(
|
|
597
|
+
connect(self.ws_url, **self._options), timeout=10.0
|
|
598
|
+
)
|
|
599
|
+
self._receiving_task = asyncio.get_running_loop().create_task(
|
|
600
|
+
self._start_receiving()
|
|
601
|
+
)
|
|
602
|
+
self._initialized = True
|
|
603
|
+
finally:
|
|
604
|
+
self._is_connecting = False
|
|
605
|
+
|
|
606
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
607
|
+
self._is_closing = True
|
|
608
|
+
try:
|
|
609
|
+
if not self._is_connecting:
|
|
610
|
+
self._in_use -= 1
|
|
611
|
+
if self._exit_task is not None:
|
|
612
|
+
self._exit_task.cancel()
|
|
613
|
+
try:
|
|
614
|
+
await self._exit_task
|
|
615
|
+
except asyncio.CancelledError:
|
|
616
|
+
pass
|
|
617
|
+
if self._in_use == 0 and self.ws is not None:
|
|
618
|
+
self._open_subscriptions = 0
|
|
619
|
+
self._exit_task = asyncio.create_task(self._exit_with_timer())
|
|
620
|
+
finally:
|
|
621
|
+
self._is_closing = False
|
|
587
622
|
|
|
588
623
|
async def _exit_with_timer(self):
|
|
589
624
|
"""
|
|
@@ -597,28 +632,27 @@ class Websocket:
|
|
|
597
632
|
pass
|
|
598
633
|
|
|
599
634
|
async def shutdown(self):
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
self._receiving_task = None
|
|
635
|
+
self._is_closing = True
|
|
636
|
+
try:
|
|
637
|
+
await asyncio.wait_for(self._cancel(), timeout=10.0)
|
|
638
|
+
except asyncio.TimeoutError:
|
|
639
|
+
pass
|
|
640
|
+
self.ws = None
|
|
641
|
+
self._initialized = False
|
|
642
|
+
self._receiving_task = None
|
|
643
|
+
self._is_closing = False
|
|
610
644
|
|
|
611
645
|
async def _recv(self) -> None:
|
|
612
646
|
try:
|
|
613
647
|
# TODO consider wrapping this in asyncio.wait_for and use that for the timeout logic
|
|
614
|
-
|
|
648
|
+
recd = await self.ws.recv(decode=False)
|
|
649
|
+
if self._log_raw_websockets:
|
|
650
|
+
raw_websocket_logger.debug(f"WEBSOCKET_RECEIVE> {recd.decode()}")
|
|
651
|
+
response = json.loads(recd)
|
|
615
652
|
self.last_received = await self.loop_time()
|
|
616
|
-
async with self._lock:
|
|
617
|
-
# note that these 'subscriptions' are all waiting sent messages which have not received
|
|
618
|
-
# responses, and thus are not the same as RPC 'subscriptions', which are unique
|
|
619
|
-
self._open_subscriptions -= 1
|
|
620
653
|
if "id" in response:
|
|
621
654
|
self._received[response["id"]] = response
|
|
655
|
+
self._in_use_ids.remove(response["id"])
|
|
622
656
|
elif "params" in response:
|
|
623
657
|
self._received[response["params"]["subscription"]] = response
|
|
624
658
|
else:
|
|
@@ -635,8 +669,7 @@ class Websocket:
|
|
|
635
669
|
except asyncio.CancelledError:
|
|
636
670
|
pass
|
|
637
671
|
except ConnectionClosed:
|
|
638
|
-
|
|
639
|
-
await self.connect(force=True)
|
|
672
|
+
await self.connect(force=True)
|
|
640
673
|
|
|
641
674
|
async def send(self, payload: dict) -> int:
|
|
642
675
|
"""
|
|
@@ -649,15 +682,20 @@ class Websocket:
|
|
|
649
682
|
id: the internal ID of the request (incremented int)
|
|
650
683
|
"""
|
|
651
684
|
original_id = get_next_id()
|
|
685
|
+
while original_id in self._in_use_ids:
|
|
686
|
+
original_id = get_next_id()
|
|
687
|
+
self._in_use_ids.add(original_id)
|
|
652
688
|
# self._open_subscriptions += 1
|
|
653
689
|
await self.max_subscriptions.acquire()
|
|
654
690
|
try:
|
|
655
|
-
|
|
691
|
+
to_send = {**payload, **{"id": original_id}}
|
|
692
|
+
if self._log_raw_websockets:
|
|
693
|
+
raw_websocket_logger.debug(f"WEBSOCKET_SEND> {to_send}")
|
|
694
|
+
await self.ws.send(json.dumps(to_send))
|
|
656
695
|
self.last_sent = await self.loop_time()
|
|
657
696
|
return original_id
|
|
658
697
|
except (ConnectionClosed, ssl.SSLError, EOFError):
|
|
659
|
-
|
|
660
|
-
await self.connect(force=True)
|
|
698
|
+
await self.connect(force=True)
|
|
661
699
|
|
|
662
700
|
async def retrieve(self, item_id: int) -> Optional[dict]:
|
|
663
701
|
"""
|
|
@@ -674,7 +712,7 @@ class Websocket:
|
|
|
674
712
|
self.max_subscriptions.release()
|
|
675
713
|
return item
|
|
676
714
|
except KeyError:
|
|
677
|
-
await asyncio.sleep(0.
|
|
715
|
+
await asyncio.sleep(0.1)
|
|
678
716
|
return None
|
|
679
717
|
|
|
680
718
|
|
|
@@ -691,6 +729,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
691
729
|
max_retries: int = 5,
|
|
692
730
|
retry_timeout: float = 60.0,
|
|
693
731
|
_mock: bool = False,
|
|
732
|
+
_log_raw_websockets: bool = False,
|
|
733
|
+
ws_shutdown_timer: float = 5.0,
|
|
694
734
|
):
|
|
695
735
|
"""
|
|
696
736
|
The asyncio-compatible version of the subtensor interface commands we use in bittensor. It is important to
|
|
@@ -708,6 +748,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
708
748
|
max_retries: number of times to retry RPC requests before giving up
|
|
709
749
|
retry_timeout: how to long wait since the last ping to retry the RPC request
|
|
710
750
|
_mock: whether to use mock version of the subtensor interface
|
|
751
|
+
_log_raw_websockets: whether to log raw websocket requests during RPC requests
|
|
752
|
+
ws_shutdown_timer: how long after the last connection your websocket should close
|
|
711
753
|
|
|
712
754
|
"""
|
|
713
755
|
self.max_retries = max_retries
|
|
@@ -715,16 +757,20 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
715
757
|
self.chain_endpoint = url
|
|
716
758
|
self.url = url
|
|
717
759
|
self._chain = chain_name
|
|
760
|
+
self._log_raw_websockets = _log_raw_websockets
|
|
718
761
|
if not _mock:
|
|
719
762
|
self.ws = Websocket(
|
|
720
763
|
url,
|
|
764
|
+
_log_raw_websockets=_log_raw_websockets,
|
|
721
765
|
options={
|
|
722
766
|
"max_size": self.ws_max_size,
|
|
723
767
|
"write_limit": 2**16,
|
|
724
768
|
},
|
|
769
|
+
shutdown_timer=ws_shutdown_timer,
|
|
725
770
|
)
|
|
726
771
|
else:
|
|
727
772
|
self.ws = AsyncMock(spec=Websocket)
|
|
773
|
+
|
|
728
774
|
self._lock = asyncio.Lock()
|
|
729
775
|
self.config = {
|
|
730
776
|
"use_remote_preset": use_remote_preset,
|
|
@@ -748,6 +794,12 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
748
794
|
self.registry_type_map = {}
|
|
749
795
|
self.type_id_to_name = {}
|
|
750
796
|
self._mock = _mock
|
|
797
|
+
self._block_hash_fetcher = CachedFetcher(512, self._get_block_hash)
|
|
798
|
+
self._parent_hash_fetcher = CachedFetcher(512, self._get_parent_block_hash)
|
|
799
|
+
self._runtime_info_fetcher = CachedFetcher(16, self._get_block_runtime_info)
|
|
800
|
+
self._runtime_version_for_fetcher = CachedFetcher(
|
|
801
|
+
512, self._get_block_runtime_version_for
|
|
802
|
+
)
|
|
751
803
|
|
|
752
804
|
async def __aenter__(self):
|
|
753
805
|
if not self._mock:
|
|
@@ -1869,9 +1921,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
1869
1921
|
|
|
1870
1922
|
return runtime.metadata_v15
|
|
1871
1923
|
|
|
1872
|
-
@a.lru_cache(maxsize=512)
|
|
1873
1924
|
async def get_parent_block_hash(self, block_hash):
|
|
1874
|
-
return await self.
|
|
1925
|
+
return await self._parent_hash_fetcher.execute(block_hash)
|
|
1875
1926
|
|
|
1876
1927
|
async def _get_parent_block_hash(self, block_hash):
|
|
1877
1928
|
block_header = await self.rpc_request("chain_getHeader", [block_hash])
|
|
@@ -1916,9 +1967,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
1916
1967
|
"Unknown error occurred during retrieval of events"
|
|
1917
1968
|
)
|
|
1918
1969
|
|
|
1919
|
-
@a.lru_cache(maxsize=16)
|
|
1920
1970
|
async def get_block_runtime_info(self, block_hash: str) -> dict:
|
|
1921
|
-
return await self.
|
|
1971
|
+
return await self._runtime_info_fetcher.execute(block_hash)
|
|
1922
1972
|
|
|
1923
1973
|
get_block_runtime_version = get_block_runtime_info
|
|
1924
1974
|
|
|
@@ -1929,9 +1979,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
1929
1979
|
response = await self.rpc_request("state_getRuntimeVersion", [block_hash])
|
|
1930
1980
|
return response.get("result")
|
|
1931
1981
|
|
|
1932
|
-
@a.lru_cache(maxsize=512)
|
|
1933
1982
|
async def get_block_runtime_version_for(self, block_hash: str):
|
|
1934
|
-
return await self.
|
|
1983
|
+
return await self._runtime_version_for_fetcher.execute(block_hash)
|
|
1935
1984
|
|
|
1936
1985
|
async def _get_block_runtime_version_for(self, block_hash: str):
|
|
1937
1986
|
"""
|
|
@@ -2137,6 +2186,7 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
2137
2186
|
storage_item,
|
|
2138
2187
|
result_handler,
|
|
2139
2188
|
)
|
|
2189
|
+
|
|
2140
2190
|
request_manager.add_response(
|
|
2141
2191
|
item_id, decoded_response, complete
|
|
2142
2192
|
)
|
|
@@ -2149,14 +2199,14 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
2149
2199
|
and current_time - self.ws.last_sent >= self.retry_timeout
|
|
2150
2200
|
):
|
|
2151
2201
|
if attempt >= self.max_retries:
|
|
2152
|
-
logger.
|
|
2202
|
+
logger.error(
|
|
2153
2203
|
f"Timed out waiting for RPC requests {attempt} times. Exiting."
|
|
2154
2204
|
)
|
|
2155
2205
|
raise MaxRetriesExceeded("Max retries reached.")
|
|
2156
2206
|
else:
|
|
2157
2207
|
self.ws.last_received = time.time()
|
|
2158
2208
|
await self.ws.connect(force=True)
|
|
2159
|
-
logger.
|
|
2209
|
+
logger.warning(
|
|
2160
2210
|
f"Timed out waiting for RPC requests. "
|
|
2161
2211
|
f"Retrying attempt {attempt + 1} of {self.max_retries}"
|
|
2162
2212
|
)
|
|
@@ -2223,9 +2273,8 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
2223
2273
|
]
|
|
2224
2274
|
result = await self._make_rpc_request(payloads, result_handler=result_handler)
|
|
2225
2275
|
if "error" in result[payload_id][0]:
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2228
|
-
in result[payload_id][0]["error"]["message"]
|
|
2276
|
+
if "Failed to get runtime version" in (
|
|
2277
|
+
err_msg := result[payload_id][0]["error"]["message"]
|
|
2229
2278
|
):
|
|
2230
2279
|
logger.warning(
|
|
2231
2280
|
"Failed to get runtime. Re-fetching from chain, and retrying."
|
|
@@ -2234,15 +2283,21 @@ class AsyncSubstrateInterface(SubstrateMixin):
|
|
|
2234
2283
|
return await self.rpc_request(
|
|
2235
2284
|
method, params, result_handler, block_hash, reuse_block_hash
|
|
2236
2285
|
)
|
|
2237
|
-
|
|
2286
|
+
elif (
|
|
2287
|
+
"Client error: Api called for an unknown Block: State already discarded"
|
|
2288
|
+
in err_msg
|
|
2289
|
+
):
|
|
2290
|
+
bh = err_msg.split("State already discarded for ")[1].strip()
|
|
2291
|
+
raise StateDiscardedError(bh)
|
|
2292
|
+
else:
|
|
2293
|
+
raise SubstrateRequestException(err_msg)
|
|
2238
2294
|
if "result" in result[payload_id][0]:
|
|
2239
2295
|
return result[payload_id][0]
|
|
2240
2296
|
else:
|
|
2241
2297
|
raise SubstrateRequestException(result[payload_id][0])
|
|
2242
2298
|
|
|
2243
|
-
@a.lru_cache(maxsize=512)
|
|
2244
2299
|
async def get_block_hash(self, block_id: int) -> str:
|
|
2245
|
-
return await self.
|
|
2300
|
+
return await self._block_hash_fetcher.execute(block_id)
|
|
2246
2301
|
|
|
2247
2302
|
async def _get_block_hash(self, block_id: int) -> str:
|
|
2248
2303
|
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,13 +117,19 @@ class RetrySyncSubstrate(SubstrateInterface):
|
|
|
117
117
|
max_retries: int = 5,
|
|
118
118
|
retry_timeout: float = 60.0,
|
|
119
119
|
_mock: bool = False,
|
|
120
|
+
_log_raw_websockets: bool = False,
|
|
121
|
+
archive_nodes: Optional[list[str]] = None,
|
|
120
122
|
):
|
|
121
123
|
fallback_chains = fallback_chains or []
|
|
124
|
+
archive_nodes = archive_nodes or []
|
|
122
125
|
self.fallback_chains = (
|
|
123
126
|
iter(fallback_chains)
|
|
124
127
|
if not retry_forever
|
|
125
128
|
else cycle(fallback_chains + [url])
|
|
126
129
|
)
|
|
130
|
+
self.archive_nodes = (
|
|
131
|
+
iter(archive_nodes) if not retry_forever else cycle(archive_nodes)
|
|
132
|
+
)
|
|
127
133
|
self.use_remote_preset = use_remote_preset
|
|
128
134
|
self.chain_name = chain_name
|
|
129
135
|
self._mock = _mock
|
|
@@ -146,6 +152,7 @@ class RetrySyncSubstrate(SubstrateInterface):
|
|
|
146
152
|
_mock=_mock,
|
|
147
153
|
retry_timeout=retry_timeout,
|
|
148
154
|
max_retries=max_retries,
|
|
155
|
+
_log_raw_websockets=_log_raw_websockets,
|
|
149
156
|
)
|
|
150
157
|
initialized = True
|
|
151
158
|
logger.info(f"Connected to {chain_url}")
|
|
@@ -174,9 +181,12 @@ class RetrySyncSubstrate(SubstrateInterface):
|
|
|
174
181
|
EOFError,
|
|
175
182
|
ConnectionClosed,
|
|
176
183
|
TimeoutError,
|
|
184
|
+
socket.gaierror,
|
|
185
|
+
StateDiscardedError,
|
|
177
186
|
) as e:
|
|
187
|
+
use_archive = isinstance(e, StateDiscardedError)
|
|
178
188
|
try:
|
|
179
|
-
self._reinstantiate_substrate(e)
|
|
189
|
+
self._reinstantiate_substrate(e, use_archive=use_archive)
|
|
180
190
|
return method_(*args, **kwargs)
|
|
181
191
|
except StopIteration:
|
|
182
192
|
logger.error(
|
|
@@ -184,10 +194,19 @@ class RetrySyncSubstrate(SubstrateInterface):
|
|
|
184
194
|
)
|
|
185
195
|
raise MaxRetriesExceeded
|
|
186
196
|
|
|
187
|
-
def _reinstantiate_substrate(
|
|
188
|
-
|
|
197
|
+
def _reinstantiate_substrate(
|
|
198
|
+
self, e: Optional[Exception] = None, use_archive: bool = False
|
|
199
|
+
) -> None:
|
|
200
|
+
if use_archive:
|
|
201
|
+
bh = getattr(e, "block_hash", "Unknown Block Hash")
|
|
202
|
+
logger.info(
|
|
203
|
+
f"Attempt made to {bh} failed for state discarded. Attempting to switch to archive node."
|
|
204
|
+
)
|
|
205
|
+
next_network = next(self.archive_nodes)
|
|
206
|
+
else:
|
|
207
|
+
next_network = next(self.fallback_chains)
|
|
189
208
|
self.ws.close()
|
|
190
|
-
if e
|
|
209
|
+
if isinstance(e, MaxRetriesExceeded):
|
|
191
210
|
logger.error(
|
|
192
211
|
f"Max retries exceeded with {self.url}. Retrying with {next_network}."
|
|
193
212
|
)
|
|
@@ -243,13 +262,20 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
|
|
|
243
262
|
max_retries: int = 5,
|
|
244
263
|
retry_timeout: float = 60.0,
|
|
245
264
|
_mock: bool = False,
|
|
265
|
+
_log_raw_websockets: bool = False,
|
|
266
|
+
archive_nodes: Optional[list[str]] = None,
|
|
267
|
+
ws_shutdown_timer: float = 5.0,
|
|
246
268
|
):
|
|
247
269
|
fallback_chains = fallback_chains or []
|
|
270
|
+
archive_nodes = archive_nodes or []
|
|
248
271
|
self.fallback_chains = (
|
|
249
272
|
iter(fallback_chains)
|
|
250
273
|
if not retry_forever
|
|
251
274
|
else cycle(fallback_chains + [url])
|
|
252
275
|
)
|
|
276
|
+
self.archive_nodes = (
|
|
277
|
+
iter(archive_nodes) if not retry_forever else cycle(archive_nodes)
|
|
278
|
+
)
|
|
253
279
|
self.use_remote_preset = use_remote_preset
|
|
254
280
|
self.chain_name = chain_name
|
|
255
281
|
self._mock = _mock
|
|
@@ -265,6 +291,8 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
|
|
|
265
291
|
_mock=_mock,
|
|
266
292
|
retry_timeout=retry_timeout,
|
|
267
293
|
max_retries=max_retries,
|
|
294
|
+
_log_raw_websockets=_log_raw_websockets,
|
|
295
|
+
ws_shutdown_timer=ws_shutdown_timer,
|
|
268
296
|
)
|
|
269
297
|
self._original_methods = {
|
|
270
298
|
method: getattr(self, method) for method in RETRY_METHODS
|
|
@@ -272,9 +300,18 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
|
|
|
272
300
|
for method in RETRY_METHODS:
|
|
273
301
|
setattr(self, method, partial(self._retry, method))
|
|
274
302
|
|
|
275
|
-
async def _reinstantiate_substrate(
|
|
276
|
-
|
|
277
|
-
|
|
303
|
+
async def _reinstantiate_substrate(
|
|
304
|
+
self, e: Optional[Exception] = None, use_archive: bool = False
|
|
305
|
+
) -> None:
|
|
306
|
+
if use_archive:
|
|
307
|
+
bh = getattr(e, "block_hash", "Unknown Block Hash")
|
|
308
|
+
logger.info(
|
|
309
|
+
f"Attempt made to {bh} failed for state discarded. Attempting to switch to archive node."
|
|
310
|
+
)
|
|
311
|
+
next_network = next(self.archive_nodes)
|
|
312
|
+
else:
|
|
313
|
+
next_network = next(self.fallback_chains)
|
|
314
|
+
if isinstance(e, MaxRetriesExceeded):
|
|
278
315
|
logger.error(
|
|
279
316
|
f"Max retries exceeded with {self.url}. Retrying with {next_network}."
|
|
280
317
|
)
|
|
@@ -314,11 +351,13 @@ class RetryAsyncSubstrate(AsyncSubstrateInterface):
|
|
|
314
351
|
ConnectionClosed,
|
|
315
352
|
EOFError,
|
|
316
353
|
socket.gaierror,
|
|
354
|
+
StateDiscardedError,
|
|
317
355
|
) as e:
|
|
356
|
+
use_archive = isinstance(e, StateDiscardedError)
|
|
318
357
|
try:
|
|
319
|
-
await self._reinstantiate_substrate(e)
|
|
358
|
+
await self._reinstantiate_substrate(e, use_archive=use_archive)
|
|
320
359
|
return await method_(*args, **kwargs)
|
|
321
|
-
except
|
|
360
|
+
except StopIteration:
|
|
322
361
|
logger.error(
|
|
323
362
|
f"Max retries exceeded with {self.url}. No more fallback chains."
|
|
324
363
|
)
|
|
@@ -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 (
|
|
@@ -52,6 +53,7 @@ from async_substrate_interface.type_registry import _TYPE_REGISTRY
|
|
|
52
53
|
ResultHandler = Callable[[dict, Any], tuple[dict, bool]]
|
|
53
54
|
|
|
54
55
|
logger = logging.getLogger("async_substrate_interface")
|
|
56
|
+
raw_websocket_logger = logging.getLogger("raw_websocket")
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
class ExtrinsicReceipt:
|
|
@@ -484,6 +486,7 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
484
486
|
max_retries: int = 5,
|
|
485
487
|
retry_timeout: float = 60.0,
|
|
486
488
|
_mock: bool = False,
|
|
489
|
+
_log_raw_websockets: bool = False,
|
|
487
490
|
):
|
|
488
491
|
"""
|
|
489
492
|
The sync compatible version of the subtensor interface commands we use in bittensor. Use this instance only
|
|
@@ -500,6 +503,7 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
500
503
|
max_retries: number of times to retry RPC requests before giving up
|
|
501
504
|
retry_timeout: how to long wait since the last ping to retry the RPC request
|
|
502
505
|
_mock: whether to use mock version of the subtensor interface
|
|
506
|
+
_log_raw_websockets: whether to log raw websocket requests during RPC requests
|
|
503
507
|
|
|
504
508
|
"""
|
|
505
509
|
self.max_retries = max_retries
|
|
@@ -526,6 +530,7 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
526
530
|
self.registry_type_map = {}
|
|
527
531
|
self.type_id_to_name = {}
|
|
528
532
|
self._mock = _mock
|
|
533
|
+
self.log_raw_websockets = _log_raw_websockets
|
|
529
534
|
if not _mock:
|
|
530
535
|
self.ws = self.connect(init=True)
|
|
531
536
|
self.initialize()
|
|
@@ -1830,12 +1835,18 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
1830
1835
|
ws = self.connect(init=False if attempt == 1 else True)
|
|
1831
1836
|
for payload in payloads:
|
|
1832
1837
|
item_id = get_next_id()
|
|
1833
|
-
|
|
1838
|
+
to_send = {**payload["payload"], **{"id": item_id}}
|
|
1839
|
+
if self.log_raw_websockets:
|
|
1840
|
+
raw_websocket_logger.debug(f"WEBSOCKET_SEND> {to_send}")
|
|
1841
|
+
ws.send(json.dumps(to_send))
|
|
1834
1842
|
request_manager.add_request(item_id, payload["id"])
|
|
1835
1843
|
|
|
1836
1844
|
while True:
|
|
1837
1845
|
try:
|
|
1838
|
-
|
|
1846
|
+
recd = ws.recv(timeout=self.retry_timeout, decode=False)
|
|
1847
|
+
if self.log_raw_websockets:
|
|
1848
|
+
raw_websocket_logger.debug(f"WEBSOCKET_RECEIVE> {recd.decode()}")
|
|
1849
|
+
response = json.loads(recd)
|
|
1839
1850
|
except (TimeoutError, ConnectionClosed):
|
|
1840
1851
|
if attempt >= self.max_retries:
|
|
1841
1852
|
logger.warning(
|
|
@@ -1944,9 +1955,8 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
1944
1955
|
]
|
|
1945
1956
|
result = self._make_rpc_request(payloads, result_handler=result_handler)
|
|
1946
1957
|
if "error" in result[payload_id][0]:
|
|
1947
|
-
if (
|
|
1948
|
-
|
|
1949
|
-
in result[payload_id][0]["error"]["message"]
|
|
1958
|
+
if "Failed to get runtime version" in (
|
|
1959
|
+
err_msg := result[payload_id][0]["error"]["message"]
|
|
1950
1960
|
):
|
|
1951
1961
|
logger.warning(
|
|
1952
1962
|
"Failed to get runtime. Re-fetching from chain, and retrying."
|
|
@@ -1955,7 +1965,14 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
1955
1965
|
return self.rpc_request(
|
|
1956
1966
|
method, params, result_handler, block_hash, reuse_block_hash
|
|
1957
1967
|
)
|
|
1958
|
-
|
|
1968
|
+
elif (
|
|
1969
|
+
"Client error: Api called for an unknown Block: State already discarded"
|
|
1970
|
+
in err_msg
|
|
1971
|
+
):
|
|
1972
|
+
bh = err_msg.split("State already discarded for ")[1].strip()
|
|
1973
|
+
raise StateDiscardedError(bh)
|
|
1974
|
+
else:
|
|
1975
|
+
raise SubstrateRequestException(err_msg)
|
|
1959
1976
|
if "result" in result[payload_id][0]:
|
|
1960
1977
|
return result[payload_id][0]
|
|
1961
1978
|
else:
|
|
@@ -2497,13 +2514,13 @@ class SubstrateInterface(SubstrateMixin):
|
|
|
2497
2514
|
Returns:
|
|
2498
2515
|
ScaleType from the runtime call
|
|
2499
2516
|
"""
|
|
2500
|
-
self.init_runtime(block_hash=block_hash)
|
|
2517
|
+
runtime = self.init_runtime(block_hash=block_hash)
|
|
2501
2518
|
|
|
2502
2519
|
if params is None:
|
|
2503
2520
|
params = {}
|
|
2504
2521
|
|
|
2505
2522
|
try:
|
|
2506
|
-
metadata_v15_value =
|
|
2523
|
+
metadata_v15_value = runtime.metadata_v15.value()
|
|
2507
2524
|
|
|
2508
2525
|
apis = {entry["name"]: entry for entry in metadata_v15_value["apis"]}
|
|
2509
2526
|
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.
|
|
3
|
+
Version: 1.3.1
|
|
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>
|
{async_substrate_interface-1.2.2 → async_substrate_interface-1.3.1}/tests/test_substrate_addons.py
RENAMED
|
@@ -4,8 +4,12 @@ import subprocess
|
|
|
4
4
|
import pytest
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
|
-
from async_substrate_interface
|
|
8
|
-
from async_substrate_interface.
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|